You can sign up to get a daily email of our articles, see the Mailing List page.
We do often include affiliate links to earn us some pennies. See more here.

Disassembling NWScript bytecode with the open source project 'xoreos'

By - | Views: 12,136

xoreos is a FLOSS project aiming to reimplement BioWare's Aurora engine (and derivatives), covering their games starting with Neverwinter Nights and potentially up to Dragon Age II. This post gives a short update on the current progress.

Note: This is a cross-post of a news item on the xoreos website.

As I already said in last year's retrospective, I want to write a bit about NWScript and its bytecode.

First of all, what is NWScript? NWScript is the scripting language and system BioWare introduced with Neverwinter Nights and used, with improvements and changes, throughout the Aurora-based games. Specifically, you can find NWScript driving the high-level game logic of Neverwinter Nights, Neverwinter Nights 2, Knights of the Old Republic, Knights of the Old Republic II, Jade Empire, The Witcher (in combination with Lua), Dragon Age: Origins and Dragon Age II. This is nearly every single game xoreos targets. The only exception is the Nintendo DS game, Sonic Chronicles: The Dark Brotherhood, which doesn't seem to use any scripts at all.

NWScript is written in a C-like language and saved with the .nss extension. A compiler then translates it into a stack-based bytecode with the .ncs extension, which is what the game executes. That is similar to how ActionScript in Flash videos works, and how Java, Lua and other scripting languages can operate.

Like C, NWScript is a strongly typed language: each variable has one definite type. Among the available types are "int" (32-bit signed integer), "float" (32-bit IEEE floating point), "string" (a textual ASCII string) and "object" (an object in the game world, like an NPC or a chest). Moreover, there are several engine types, like "event" and "effect", though which of these are available depends on the game. There are also structs, but in the compiled bytecode, they vanish and are replaced by a collection of loose variables. Likewise, the "vector" type is treated as three single float variables. A special type is the "action" type, a script state (or functor) that's stored separately.

Additionally, Dragon Age: Origins adds a "resource" type (which, in the bytecode, can be used interchangeably with the "string" type) and dynamic arrays. Dragon Age II in turn adds the concept of a reference to a variable to help performance in several use-cases. For these new features, these two games each add two new bytecode opcodes, something not done for any of the other post-Neverwinter Nights games.

To get and modify the state of the game world, like searching for objects and moving them, and for complex mathematical operations like trigonometry functions, the NWScript bytecode can call so-called engine functions. These are functions that are provided by the game itself; about 850 per game, with some overlap. They're declared in the nwscript.nss file (nwscriptdefn.nss for The Witcher and script.ldf for the Dragon Age games) of each game.

The original Neverwinter Nights toolset came with a compiler, but a part of the modding community, the OpenKnights Consortium, created their own, free software compiler, nwnnsscomp. Unfortunately, it has a few disadvantages. For example, it always needs the nwscript.nss file and it also only handles Neverwinter Nights. And while there has been several variations that have been extended to handle newer games, many of these are only available as Windows binaries. As far as I'm aware, there never has been a variation that handles Dragon Age: Origins or Dragon Age II. Also, since the code hasn't been touched for 10 years, it's difficult to compile now, and it doesn't work when compiled for 64-bit. For what it's worth, I mirrored the old OpenKnights NWNTools, with a few changes, to GitHub here.

This nwnnsscomp also has a disassembly mode, which can convert the compiled bytecode into somewhat human-readable assembly. This is pretty useful! I wanted my own disassembler in xoreos-tools.

The steps to disassemble NWScript bytecode are the following:

1) Read instructions

Read the .ncs file, instruction for instruction. An instruction consists of an opcode (like ADD for addition), the argument types (which are taken from or pushed onto the stack) and any direct arguments. For example, an addition that operates on two integers would be known as ADDII. The instructions are stored in a list, one after the other.

2) Link instructions

Each instruction may have a follower, the instruction that follows naturally. For most instructions, this is the instruction next in the list. But certain branching instruction, jumps and subroutine calls, also have jump destinations that may be taken.

3) Create blocks

Group the instructions into blocks. A block is a sequence of instructions that follow each other, with two constraints: a jump into a block can only land at the beginning of a block and a jump out of a block can only happen at the end of the block.

4) Create subroutines

Group the blocks into subroutines. A subroutine is a sequence of blocks that gets jumped to by a special opcode, JSR, and returns to the place from where it has been called with RETN. (In many programming languages, for example C, these are also called functions, but we're calling them subroutines so that they're not being confused with engine functions. Subroutine is also often the usual term in assembly dialects.)

5) Link subroutines

Record where a subroutine calls another and link the caller and callee, so that a call graph could be created easily. Likewise, the instructions that start and end the subroutine are also separately recorded.

6) Identify "special" subroutines

There's three special subroutine that we can identify:

  • _start(), the very first subroutine that starts execution of the script. It's the subroutine that contains the very first instruction in the .ncs file.
  • _global(), which, if it exists, sets up the global variables. This is the subroutine that contains an instruction with a SAVEBP opcode.
  • main(), which is the main or StartingConditional function visible in the script source. If a _global() subroutine exists, this is the last subroutine called by _global(). Otherwise, it's the last (and only) subroutine called by _start().


7) Analyze the stack

This step goes through the whole script and evaluates each instruction for how it manipulates the stack. Since stack elements are explicitly typed, and instructions that create new stack elements know which type they create (either explicitly, or implicitly by copying an already typed element), both the size of the stack and the type of all elements can be figured out. At the end, each instruction will know how the stack looks before its execution. And for each subroutine, we then know its signature: how many parameters it takes, what their types are and what the subroutine returns.

However, this step only works if we know which game this script is from, because we need to know the signatures of the engine functions. And, unfortunately, this step fails when the script subroutines call each other recursively. The stack of a recursing script can't be analyzed like this.

8) Analyze the control flow

So far, the script disassembly consists of blocks that jump to each other, with no further meaning attached. To extract this deeper meaning, we analyze the control flow for higher-level control structures: do-while loops, while loops, if and if-else blocks, together with break and continue statements and early subroutine returns. Each block gets a list of designators that show if and how it contributes to such a control structure.

9) Create output

Finally, we create an output. This can be one of three:

  • A full listing, including offset, raw bytes and disassembly. This is similar to the output created by the disassembly mode of the OpenKnights nwnnsscomp.
  • Only the disassembly. This output might be used to reassemble the script some time later, should someone want to write an assembler.
  • A DOT file. A DOT file is a textual description of a graph, which can be plotted into a graph with the Graphviz tool. The result is a clear representation of the control flow in graph form.


As an example, here's a script from Neverwinter Nights 2: this is the original source, and this is the full listing output, the assembly-only output and the control flow graph.

During this work, I have found a few interesting little bugs in the original BioWare NWScript compiler. For example, this little script, hf_c_skill_value.nss in Neverwinter Nights 2 and its disassembly:

int StartingConditional(int nSkill, int nValue)
{
object oPC = GetPCSpeaker();
int nSkill = GetSkillRank(nSkill, oPC);
return (nSkill >= nValue);
}


00000017 02 06 RSADDO
00000019 05 00 00EE 00 ACTION GetPCSpeaker 0
0000001E 01 01 FFFFFFF8 0004 CPDOWNSP -8 4
00000026 1B 00 FFFFFFFC MOVSP -4
0000002C 02 03 RSADDI
0000002E 04 03 00000000 CONSTI 0
00000034 03 01 FFFFFFF4 0004 CPTOPSP -12 4
0000003C 03 01 FFFFFFF4 0004 CPTOPSP -12 4
00000044 05 00 013B 03 ACTION GetSkillRank 3


Specifically, the int nSkill = GetSkillRank(nSkill, oPC); line is compiled wrong. First, an instruction with the opcode RSADDI is generated, which creates a new integer variable on the stack, for nSkill. Then, the arguments for GetSkillRank are pushed onto the stack, and GetSkillRank is called using the ACTION instruction.

Unfortunately, as soon as the compiler creates the stack space for the local variable nSkill, it associates nSkill with this local variable. So when it's time to push the parameter nSkill for GetSkillRank on the stack, the parameter to the outer StartingConditional subroutine has already been overruled, and the CPTOPSP points to the new local variable.

This renders the nSkill parameter unused and useless, and GetSkillRank is potentially called with an uninitialized value.

For another example, have a look at this script from Neverwinter Nights:

int StartingConditional()
{
int iResult;
object oPC = GetPCSpeaker();

iResult = GetClassByPosition(1,oPC) == CLASS_TYPE_DRUID ||
GetClassByPosition(2,oPC) == CLASS_TYPE_DRUID ||
GetClassByPosition(3,oPC) == CLASS_TYPE_DRUID;
return iResult;
}


It's meant to check whether the player character is a druid. Since you can multi-class in Neverwinter Nights, it checks whether the character is a druid for the first class, for the second class and then the third class. If any of these return true, iResult will be set to true. To achieve this, a boolean disjunction ("or" operation) is used. As is customary in C-like languages, the boolean disjunction in NWScript is supposed to support short-circuiting: if the first part is already true, the second (and third) checks aren't even called.

Let's see how the disassembly graph looks like:

The first EQII is the first comparison, and then the block in loc_00000057 is supposed to do the short-circuiting. It duplicates the top-most stack element with a CPTOPSP -4 4 before bypassing the second comparison and jumping to the LOGORII that does the boolean disjunction. Unfortunately, instead of directly jumping to loc_00000080 with a JMP, a JZ was generated instead. And since the top-most stack element was already duplicated and checked with the previous JZ, we know that the true-edge is never taken. It is a dead edge.

This has an interesting consequence. The short-circuiting for the boolean disjunction is broken: all parts are always evaluated before the results are or'ed together. In practice, this doesn't matter much. It makes the code a bit slower, and any side effects will always happen.

Additionally, if the true edge were ever taken, the stack would be in a broken state. Unlike JMP, JZ consumes a stack element, and so the LOGORII would be missing one of its arguments. Because this is not possible, it doesn't matter for execution, but my stack analysis dies there. To combat this problem, I added an extra disassembly step after the block generation, the detection of these dead edges. To keep it simple, I only do some simple pattern matching, which is enough for most scripts. There are a few cases where it fails, though.

This bug is present in the original scripts coming with Neverwinter Nights, Knights of the Old Republic and Knights of the Old Republic II, but has already been fixed by the release of Neverwinter Nights 2.

This bug is also not present in the OpenKnights compiler:

As you can see, the branch instruction in loc_00000057 is a JMP, as it should be.

So, to recap, xoreos-tools now has a tool that can disassemble NWScript bytecode, similar to the disassemble mode of the OpenKnights nwnnsscomp, with these added features:

  • Easier to compile.
  • Works compiled as 64-bit.
  • Out-of-the-box support for the NWScript found in all Aurora-based games.
  • No need for a separate nwscript.nss.
  • Support for new array and reference opcodes.
  • Deeper analysis of the stack, to figure out the subroutine signatures.
  • Deeper analysis of the control flow, to detect higher-level control structures.
  • Can create control flow graphs in DOT format.

  •  

If you're interested, the source is available here. Binaries will come with the next release of xoreos and xoreos-tools.

There is, however, still a lot left to do there:

  • Create a decompiler: use the detected control structures as a base to generate C-like NWScript source code.
  • Detect chained instructions: something like "int a = b * c" compiles to a lot of instructions that create temporary stack variables.
  • Detect structs and vectors.

Unlike nwnnsscomp, xoreos-tools is still missing a compiler as well. This is something that would be very nice to have indeed. An assembler, which can take the disassembly output and create a working .ncs file out of it would probably be a useful first step in that direction.

If you would like to help and take up any of these tasks, or any other task from our TODO list, please contact us! :)

Article taken from GamingOnLinux.com.
0 Likes
About the author -
Geek. Atheist+. Leftist. Metal-Head. Discordian. Lefty.
ScummVM dev, xoreos lead.
Free software zealot.
See more from me
The comments on this article are closed.
9 comments

minj Jan 16, 2016
Just here to say that I really enjoy these technical yet fairly n00b-friendly articles. It's interesting to peak into the reverse-engineering world.
DrMcCoy Jan 17, 2016
Quoting: minjJust here to say that I really enjoy these technical yet fairly n00b-friendly articles.

Thanks. :)

Writing these is something I find quite difficult. I want to explain my work in a way that's legible for people not deep within the topic. But I also don't want to bore people who do know more to death. All the while, I need to keep it somewhat short, because it's not helpful either to clobber everyone over the head with a 100,000 words article.

Glad I'm succeeding at least partially there. If there's anything unclear, please don't hesitate to ask.
NoYzE Jan 17, 2016
I love the project!
NWN is one of my all-time-favorite games and though it runs quite well on wine i'd always love to have a wineless implementation ;)
Also it's quite cool to experience some insight into the programming of it too :D
Mountain Man Jan 17, 2016
Neverwinter Nights has a native Linux version out there somewhere.
drmoth Jan 17, 2016
Love these articles too, keep em coming! Very satisfying watching all this hacking take shape :)


Last edited by drmoth on 17 January 2016 at 10:39 am UTC
Apopas Jan 17, 2016
Congrats Dr!
Sslaxx Jan 18, 2016
Quoting: Mountain ManNeverwinter Nights has a native Linux version out there somewhere.
But like the Windows version, it's increasingly difficult to get it to run as time goes by.
Speedster Jan 20, 2016
Quoting: Sslaxx
Quoting: Mountain ManNeverwinter Nights has a native Linux version out there somewhere.
But like the Windows version, it's increasingly difficult to get it to run as time goes by.

Indeed, native Diamond edition didn't work on either ubuntu LTS or gentoo when I tried it recently.

DrMcCoy, is NWScript one of the big missing pieces for actually being able to play NWN with xoreos? Still another piece needed for combat? The wiki status page shows what *does* work rather than what is left to do...
https://wiki.xoreos.org/index.php?title=Neverwinter_Nights
DrMcCoy Jan 20, 2016
Quoting: SpeedsterDrMcCoy, is NWScript one of the big missing pieces for actually being able to play NWN with xoreos?

As such, no. There is (and already had been) a working NWScript interpreter in xoreos. What's missing there is the myriad of engine functions that the scripts call. These need to be implemented in xoreos. Currently, about 100 of 850, per game, of those are implemented.

However, having a disassembler helps debugging when things go wrong, because I made a mistake or somesuch. In fact, the work I put into that already helped me finding a wrong assumption I had made, and helped me cement another assumption. Also, figuring out those four new opcodes (two for Dragon Age: Origins, another two for Dragon Age II) was thanks to a combination of the disassembly mode of OpenKnight's nwnnsscomp and the start of this here xoreos' disassembler.

Also also, I personally find disassembling and decompilation a really interesting topic. :)

Oh, and the result might be useful for the modding communities that still exist. So even if xoreos never goes anywhere, at least I produced a few useful tools. Maybe. :P

Quoting: SpeedsterStill another piece needed for combat?

For NWN, next up would be walkmeshes, pathfinding and then putting those together with the (partially working, a bit buggy) animation system to have the characters walk. Then correct positioning of the camera, above the PC. Then evaluating triggers, areas on the floor that start scripts when something enters or leaves them.

For combat, there's way more to be done. Objects need inventories, characters need equipment slots. There needs to be a proper dividing of the time in rounds. All the combat rules need to be implemented, and all the 2DA files that specify values and things for the rules need to be read.

Quoting: SpeedsterThe wiki status page shows what *does* work rather than what is left to do...

The TODO page is rather the one with the things left to do. It's not a complete list of what's missing, either, though.
While you're here, please consider supporting GamingOnLinux on:

Reward Tiers: Patreon. Plain Donations: PayPal.

This ensures all of our main content remains totally free for everyone! Patreon supporters can also remove all adverts and sponsors! Supporting us helps bring good, fresh content. Without your continued support, we simply could not continue!

You can find even more ways to support us on this dedicated page any time. If you already are, thank you!
The comments on this article are closed.