Disassembly: Difference between revisions

From LRREW
Jump to navigation Jump to search
 
(48 intermediate revisions by 2 users not shown)
Line 1: Line 1:
Assembly is something we all have to learn eventually in order to properly modify Roblox without having its source code.
{{WIP}}


Usually, we use a tool such as IDA Pro or x32dbg. Because Roblox (before Byfron) uses VMProtect, simply modifying its executable isn't possible, and you must attach to it while its running.
'''Disassembly''' in computers generally refers to the practice of taking apart binaries (such as <code>.exe</code> files) in order to analyze them without having access to the original source code. With regards to Roblox, disassembly of binaries is necessary in order to achieve any meaningful patching, such as [[Hooking|hooks]], [[String Modification|modifying strings]], and other necessary modification. There are two sections that will be covered in this article which are immediately relevant to Roblox:
* [https://en.wikipedia.org/wiki/Assembly_language Assembly language (ASM)], a low-level programming language that all computers speak, and more specifically '''[https://en.wikipedia.org/wiki/X86_assembly_language x86 assembly]'''; assembly using the [https://en.wikipedia.org/wiki/X86_instruction_listings x86 instruction set] (as Roblox clients are all x86{{efn|Until version 0.574.1.5740447 (dated 5/3/2023), which is when Roblox introduced Byfron and dropped x86 support.}})
* '''Debugging and disassembly''' in order to find subroutine addresses to place [[Hooks|hooks]] and to make other client modifications, such as cosmetic ones


This article isn't finished yet, sorry.
Common tools used in disassembly are:
* '''[https://cutter.re/ Cutter]''' : A cross-platform fast disassembler with a much more welcome welcoming user interface and the Ghidra decompiler included, as well as the [https://github.com/rizinorg/jsdec jsdec decompiler]. Includes an experimental debugger. This is an excellent tool to get started with disassembly. This, in combination with x64dbg and Resource Hacker create a fantastic starter kit.
* '''IDA Pro with Hex-Rays decompiler''': An all-in-one disassembler with a debugger and decompiler (from ASM to rudimentary C++) included. This is the preferred choice for most reverse engineers as it is includes state-of-the-art disassembling technology; however, a license is necessary and prices for those can go up to more than a thousand dollars. As such, most people choose to acquire it through illegitimate means, despite it being unsafe to do so. Additionally, the learning curve is steep; newcomers may be intimidated by its complicated UI.
* '''[https://ghidra-sre.org/ Ghidra]''': An alternative to IDA developed by the NSA<ref>"NSA Releases Ghidra, a Free Software Reverse Engineering Toolkit." ''ZDNET'', www.zdnet.com/article/nsa-release-ghidra-a-free-software-reverse-engineering-toolkit/.</ref>. Requires Java and has a very basic, primitive UI. However, the learning curve isn't as steep. Includes a decompiler.
* '''[https://x64dbg.com/ x64dbg]''' ''(Windows only)'': A powerful debugger for Windows. Fast, simple UI. Not too complicated to use but also not that easy. However, the learning curve is smooth and it's easy to get a hang of it.
* '''[https://mh-nexus.de/en/hxd/ HxD]''' ''(Windows only)'': A basic hex editor for Windows. Simple UI and easy to use.
* '''[http://www.angusj.com/resourcehacker/ Resource Hacker]''' ''(Windows only)'': A resource editor for Windows applications. Useful for editing the icon of the client, and to edit menus{{efn||1=On [https://learn.microsoft.com/en-us/cpp/mfc/mfc-desktop-applications?view=msvc-170 MFC] clients (before they fully migrated to Qt studio.)}} and other special metadata.


This article assumes you have basic knowledge in C++, and in general Computer Science.
This article assumes you have an intermediate knowledge in both C++ and computer science in general.


== Instructions ==
== Assembly ==
=== Instruction set ===


The x86 instruction set is a vast instruction set with various extensions. Luckily, you'll only really see basic x86 instructions when debugging Roblox as all Roblox clients (before 2023) are x86.
The x86 instruction set is a vast instruction set with various extensions. Luckily, you'll only really see basic x86 instructions when debugging Roblox.
 
These are some common instructions (but not every instruction) that can be seen whilst debugging Roblox.  


These are some common instructions (but not every instruction) that can be seen while debugging Roblox.
{| class="wikitable"
{| class="wikitable"
|+ x86 instructions (partial list)
|+ x86 instructions (partial list)
Line 18: Line 26:
! Instruction (NASM syntax) !! Name !! Purpose  
! Instruction (NASM syntax) !! Name !! Purpose  
|-
|-
| jne [address] || Jump if Not Equal || The processor will set EIP to [address], if EFLAGS has the NE (Not equal) bit set.
| {{code|lang=asm |jnz [address]}}, {{code|lang=asm |jne [address]}} || '''J'''ump if '''n'''ot '''z'''ero, '''J'''ump if '''n'''ot '''e'''qual  || The processor will set EIP to the given address if EFLAGS has the ZF ('''z'''ero '''f'''lag) bit cleared (generally set by a {{code|lang=asm | cmp}} operation).
|-
| {{code|lang=asm |jz [address]}}, {{code|lang=asm |jeq [address]}} || '''J'''ump if '''z'''ero, '''J'''ump if '''eq'''ual || The processor will set EIP to the given address if EFLAGS has the ZF ('''z'''ero '''f'''lag) bit set (generally set by a {{code|lang=asm | cmp}} operation).
|-
|-
| jnz [address] || Jump if Not Zero || The processor will set EIP to [address], if EFLAGS has the NZ (Not zero) bit set.
| {{code|lang=asm |jmp [address]}} || '''J'''u'''mp''' || The processor will set EIP to the given address.
|-
|-
| call [address] || CALL || The processor will set EIP to [address], then push the current address.
| {{code|lang=asm |call [address]}} || '''Call''' || The processor will set EIP to the given address and then push the current address to the stack.
|-
|-
| cmp [a], [b] || CoMPare || The processor will compare [a] and [b], and set EFLAGS with the results of the comparison.
| {{code|lang=asm |cmp [A], [B]}} || '''C'''o'''mp'''are || The processor will compare addresses A and B, and set EFLAGS to the result of the comparison.
|-
|-
| mov [a], [b] || MOVe || The processor will set [b] to [a].
| {{code|lang=asm |mov [A], [B]}} || '''Mov'''e || The processor will set B to A.
|-
|-
| nop || NO oPeration || The processor will not do anything.
| {{code|lang=asm |nop}} || '''No''' o'''p'''eration || The processor will not do anything. This is generally only used for debugging.
|}
|}


== Where's All the Data? ==
Why does {{code|lang=asm |jne}} and {{code|lang=asm |jnz}} have the same purpose? Its largely for programmer readability, but {{code|lang=asm |cmp}} sets ZF in EFLAGS if the value being compared is 0 or equals the other value. {{code|lang=asm |jz}} and {{code|lang=asm |jeq}} does this too.


It may be noticed, that in the set provided above there are terms such as 'EFLAGS', and '%eip'. These are CPU registers. CPU registers are the fastest way to retrieve, manipulate and store data but are limited in size.  
=== Where's all the data? ===
It may be noticed, that in the set provided above there are terms such as "EFLAGS" and "EIP". These are what are known as '''CPU registers'''. CPU registers are the fastest way to retrieve, manipulate, and store data, but are limited in size.  


{| class="wikitable"
{| class="wikitable"
Line 40: Line 51:
! Register !! Purpose  
! Register !! Purpose  
|-
|-
| EAX || General purpose register, sometimes called the Accumulator register
| EAX || General purpose register, sometimes called the Accumulator register. In C and C++ this is used for storing the return value of the last function.
|-
|-
| EBX || General purpose register, sometimes called the Base register
| EBX || General purpose register, sometimes called the Base register
Line 58: Line 69:
| EIP || Instruction Pointer. This is where the x86 fetches the next instruction from memory from, and is incremented by the size of the decoded instruction every instruction.
| EIP || Instruction Pointer. This is where the x86 fetches the next instruction from memory from, and is incremented by the size of the decoded instruction every instruction.
|-
|-
| EFLAGS || FLAGS. This is where the cmp instruction stores its results. This is not directly accessible by code itself, but can be manipulated via flow control using the {{code|lang=asm |JNZ}}, {{code|lang=asm |JNE}}, {{code|lang=asm |JEQ}} and miscellaneous instructions.
| EFLAGS || FLAGS. This is where the {{code|lang=asm |cmp}} instruction stores its results. This is not directly accessible by code itself, but can be manipulated via flow control using the {{code|lang=asm |jnz}}, {{code|lang=asm |jne}}, {{code|lang=asm |jeq}} and miscellaneous instructions.
|}
|}


Line 64: Line 75:


=== Memory ===
=== Memory ===
Memory is the second fastest way to store data on x86. It is a large array of sorts, storing the program itself, all of the data the program reads and writes to, and everything else necessary for system functioning.  
Memory is the second fastest way to store data on x86. It is a large array of sorts, storing the program itself, all of the data the program reads and writes to, and everything else necessary for system functioning.  


Values have different size depending on their storage type.
Values have different size depending on their storage type. Here are some storage types that are immediately relevant to us:


{| class="wikitable"
{| class="wikitable"
! Type !! sizeof(Type) in bytes !! Max Value
! Type !! Size (in bytes) !! Max Value
|-
|-
| uint64_t (long) || 8 || &#177;9,223,372,036,854,775,807 (signed), 18,446,744,073,709,551,615 (unsigned)
| {{code|lang=cpp |uint64_t}} ({{code|lang=cpp |unsigned long}}) || 8 || &#177;9,223,372,036,854,775,807 (signed), 18,446,744,073,709,551,615 (unsigned)
|-
|-
| uint32_t (int) || 4 || &#177;2,147,483,647 (signed), 4,294,967,295 (unsigned)
| {{code|lang=cpp |uint32_t}} ({{code|lang=cpp |unsigned int}}) || 4 || &#177;2,147,483,647 (signed), 4,294,967,295 (unsigned)
|-
|-
| uint16_t (short) || 2 || &#177;32,767 (signed), 65,535 (unsigned)
| {{code|lang=cpp |uint16_t}} ({{code|lang=cpp |unsigned short}}) || 2 || &#177;32,767 (signed), 65,535 (unsigned)
|-
|-
| uint8_t (char) || 1 || &#177;127 (signed), 255 (unsigned)
| {{code|lang=cpp |uint8_t}} ({{code|lang=cpp |unsigned char}}) || 1 || &#177;127 (signed), 255 (unsigned)
|}
|}


==== Unsigned and Signed Integers ====
==== Unsigned and Signed Integers ====
 
A signed integer is simply an integer with the last bit set to hold the "sign" bit. This bit determines if the integer is negative or positive. When it is 0, the integer is positive; and when it is 1, the integer is negative.
A signed integer is simply an integer with the last bit set to hold the "sign" bit. This bit determines if the integer is negative or positive. When it is 0, the integer is positive and when it is 1, the integer is negative.


==== Integer Endianess ====
==== Integer Endianess ====
{{multiple image
{{multiple image
  | header            = Endian example
  | header            = Endian example
Line 96: Line 104:
}}
}}


In x86, integers are little endian. That means that the most significant byte (the first byte in the integer) at the largest address (the address farthest from 0), and stores the least significant byte (the last byte in the integer) at the smallest address (the address closest to 0).
In x86, integers are little endian. That means that the most significant byte (the first byte in the integer) at the largest address (the address farthest from 0), and stores the least significant byte (the last byte in the integer) at the smallest address (the address closest to 0.)


=== Stacks ===
=== Stacks ===
Stacks are a form of data storage employed by most CPU architectures, including x86. In x86, the stack can be imagined as a stack of plates. You can put a plate on top of the stack (PUSH to the stack), and take the top most one off (POP from the stack).
Stacks are a form of data storage employed by most CPU architectures, including x86. In x86, the stack can be imagined as a stack of plates. You can put a plate on top of the stack (PUSH to the stack), and take the top most one off (POP from the stack).


When you call for example, the processor will PUSH the value of %eip, then go to the new address. When that subroutine eventually executes a RETurn instruction, the processor will POP the last value on the stack (which in this case, is what %eip used to be!) and then set %eip to the old address.
When you call for example, the processor will PUSH the value of EIP, then go to the new address. When that subroutine eventually executes a RETurn instruction, the processor will POP the last value on the stack (which in this case, is what EIP used to be!) and then set EIP to the old address.


The stack of plates analogy breaks when it is possible to access ANY value in the stack at ANY time without POPing it, because in x86's case it has full access to the memory of the stack. This is useful when you use a calling convention, which is explained further below.
The stack of plates analogy breaks when it is possible to access ANY value in the stack at ANY time without POPing it, because in x86's case it has full access to the memory of the stack. This is useful when you use a calling convention, which is explained further below.


=== Calling Conventions ===
=== Calling Conventions ===
{{Ambox
{{Ambox
| nocat = true
| nocat = true
| type  = content
| type  = content
| text  = The stack frames may not be correct. As of writing this, I was very sleepy and I am a bit rusted myself in Assembly.
| text  = The stack frames may not be correct. If you spot an issue, please update it!
}}
}}


To complicate stacks further, most programs employ a calling convention. This is most used by programming languages such as C, in order to keep track of data between functions. When functions run, without storing its initial registers the new function will overwrite them, and the data held beforehand will be lost. This is a problem when in this case:
To complicate stacks even further, most programs employ a calling convention. This is most used by programming languages such as C, in order to keep track of data between functions. When functions run, without storing its initial registers the new function will overwrite them, and the data held beforehand will be lost. An example :


  int x = 10; // imagine this is the register x
  int x = 10;               // imagine this is a register named "x"
  printf("Hello World"); // this function will likely need to use the register x in its lifetime
  printf("Hello World");     // this function will likely need to use register "x" in its lifetime
  printf("%i\n", x) // x returns 5, function before overwrote x 'mangling' it
  printf("%i", x)           // this will print 5 as the function before overwrote register "x", thus "mangling" it


The solution to this is to use the aforementioned stack. When a function is called, it will PUSH certain registers and then CALL the address. In our basic calling convention where the '''callee''' (the thing that calls the function) stores EAX.
The solution to this is to use the aforementioned stack. When a function is called, it will PUSH certain registers and then CALL the address. In our basic calling convention where the '''callee''' (the thing that calls the function) stores EAX.


  callee:
  callee:
  mov eax, 20
    mov eax, 20
  push eax
    push eax
  call function
    call function
  ; eax now equals 10
    ; eax now equals 10
  pop eax
    pop eax
  ; eax now equals 20
    ; eax now equals 20
  function:
  function:
  mov eax, 10
    mov eax, 10
  ret
    ret


There is a catch here, for function to manipulate data. When function reaches its first instruction, this is what memory looks like at ESP for itself:
There is a catch here, for function to manipulate data. When function reaches its first instruction, this is what memory looks like at ESP for itself:
Line 138: Line 144:
|+ x86 stack frame
|+ x86 stack frame
|-
|-
! Offset !! Name
! Offset !! Value
|-
|-
| esp-0, ebp+4 || Return Address
| <code>esp</code> || Return Address
|-
|-
| esp-4, ebp+0 || EAX
| <code>esp + 4 bytes</code> || EAX
|}
|}


Through accessing [esp-4], or [ebp+0] the function can modify the stack above it. This is used internally with functions too, to allocate values on the stack.
This is not a realistic calling convention however, as it does not store EBP (the stack frame base pointer.)


  int function(void) {
==== <code>__cdecl</code> ====
  int x = 0;
'''<code>__cdecl</code>''' is one of the calling conventions used in modern x86. It is the default calling convention used in C, and stores arguments and local variables on the stack.
  x = x + 1;
 
  return x;
Through accessing memory using ESP or EBP as offset bases the function can modify the stack, receiving arguments and allocating values on the stack.
 
void caller()
{
    function(5);
}
  int function(int y)
{
    int x = 0;
    x = x + y;
    return x;
  }
  }


This will compile to
This will be assembled as:


  caller:                ; fake function, just showing how its called
  caller:              
  push ebp
    push 5               ; push argument y to function
  call function
    call function


  function:
  function:
  push 0                ; int x = 0;
    push ebp              ; preserve current frame pointer
  mov eax, [ebp-4]
    mov esp, ebp          ; set frame pointer to stack top
  add eax, 1           ; x = x + 1
    sub esp, 4            ; allocate space for int x;
  mov [ebp-4], eax
    mov eax, [ebp-4]    
  pop eax               ; in C, eax is the return value
    mov eax, 0           ; int x = 0
  ret
    add eax, [ebp+8]     ; x = x + y
    add esp, 4            ; restore space from int x;
    pop ebp               ; restore old frame pointer
    ret                  ; eax is the return register on __cdecl


In memory, the function will see this before POPing to eax:
In memory, the function will see this before executing {{code|lang=asm |add esp, 4}}:


{| class="wikitable"
{| class="wikitable"
|+ x86 stack frame
|+ x86 stack frame
|-
|-
! Offset !! Name
! Offset !! Value
|-
|-
| esp-0, ebp+8 || int x;
| <code>esp</code>, <code>ebp - 4 bytes</code> || {{code|lang=cpp | int x;}}
|-
|-
| esp-4, ebp+4 || Return Address
| <code>esp + 4 bytes</code>, <code>ebp + 0 bytes</code> || EBP
|-
|-
| esp-8, ebp+0 || EBP (old stack frame, pushed by caller)
| <code>esp + 8 bytes </code>, <code>ebp + 4 bytes</code> || Return Address
|-
| <code>esp + 12 bytes</code>, <code>ebp + 8 bytes</code> || {{code|lang=cpp | int y;}}
|}
|}


The conventions above are imaginary calling conventions however, and are not what you would find in Roblox. Roblox uses C++ calling conventions such as '''__thiscall'''.
The conventions above are using the '''__cdecl''' conventions however, and are not what you would find in some Roblox methods. Roblox and C++ classes uses a calling convention named '''__thiscall'''.


==== __thiscall ====
==== <code>__thiscall</code> ====
 
In <code>__thiscall</code>, variable arguments are not supported. However, ECX (which you might be able to recognize now) is used as {{code|lang=cpp |this}}. All arguments are pushed onto the stack. A <code>__thiscall</code> function may look like this in ASM:
In __thiscall, variable arguments are not supported. However, ECX (a register you may recognize above) is used as {{code|lang=cpp |this}}. All arguments are pushed onto the stack. A __thiscall function in assembly may look like this:


  callee:
  callee:
  push ecx          ; save class
    push ecx          ; save class
  push ebp          ; push stack frame address
    push eax          ; argument 0
  push eax          ; argument 0
    push ebx          ; argument 1
  push ebx          ; argument 1
    mov ecx, class_ptr ; class
  mov ecx, class_ptr ; class
    call class_func    ; class' class_func
  call class_func    ; class' class_func
    add esp, 8        ; 'remove' argument 0 and 1 from stack
  add esp, 8        ; 'remove' argument 0 and 1 from stack
    pop ecx            ; restore class
  pop ebp            ; restore frame address
  pop ecx            ; restore class


This would roughly be the same as
This would roughly be the same as:


  void caller(void)
  void caller(void)
  {
  {
  class* class = 0;
    class* class = 0;
  class->class_func(0, 0);
    class->class_func(0, 0);
  }
  }


== Why does Roblox keep on stopping? ==
The function <code>class_func</code> would most likely assemble to:
 
class_class_func:
    push ebp          ; preserve current frame pointer
    mov ebp, esp      ; create new frame pointer by pointing to the current stack top
    ret
 
==== <code>__stdcall</code> ====
'''<code>__stdcall</code>''' is a Win32 specific calling convention in which the callee cleans the stack. It's not very important here, so we'll brush over this.
 
=== Modules ===
Modules are executable files loaded into memory. When Roblox starts, Win32 will load various DLLs and run them and then run Roblox. This is why if you start Roblox with x32dbg, it will start in some random DLL instead of RobloxApp.exe for example.
 
Performing operations such as searching for strings will not work in modules that are not the RobloxApp module (or whatever executable Roblox is running from) since it will search memory regions that may have nothing to do with Roblox itself.
 
When Win32 loads a module, the module within has information where certain regions of data within the module are mapped into memory. There are 4 sections which are important:
 
==== <code>.text</code> ====
'''<code>.text</code>''' is where the executable code is located. It is meant to be non-writable. Most of the time you spend disassembling will be within this section.
 
==== <code>.rodata</code> ====
'''<code>.rodata</code>''' is where global constants are located. It is meant to be non-writable as well ('''R'''ead '''o'''nly '''data'''.)
 
==== <code>.data</code> ====
'''<code>.data</code>''' is where initialized read-write data is located, such as strings.
 
==== <code>.bss</code> ====
'''<code>.bss</code>''' is where uninitialized read-write data is located.
 
==== Where's the stack? ====
The stack is not in any of these sections, and is set to an address specified by the executable upon startup.
 
=== Why does Roblox keep on stopping? ===
This is probably because you ran into an '''exception'''. Exceptions are used in C++ to signal when a function must quickly exit and return some error data to the parent function. For example, when [[Trust check|trust check]] fails it will throw a C++ exception, which is then caught, outputting an error in the Roblox console.


This is probably because you ran into an '''Exception'''. Exceptions are used on C++ to signal when a function must quickly exit and return some error data to the parent function. For example, when [[Roblox:TrustCheck|trust check]] fails it throws a C++ exception, and then outputs an error to the Roblox console.
==== I got <code>ACCESS_VIOLATION</code> instead!  ====
That probably means something broke within Roblox itself. An access violation is when the program attempts to access unallocated or unusable memory, and the OS raises an error. It signals to the program that it has done something wrong (thus an access violation.) Access violations generally get passed to Roblox's crash handler.


=== Uh... it says ACCESS_VIOLATION though... ===
=== Why is there an infinite amount of exceptions when Internet Explorer opens on old studio versions? ===
That's because Internet Explorer <s>sucks</s> tries to load a page it can't support (generally if you haven't changed the URL from <code>http://roblox.com</code> in [[AppSettings|AppSettings.xml]].


That probably means something broke with Roblox. An access violation is when the program attempts to access unallocated or unusable memory, and the OS notices this. It signals to the program "You have done something wrong" (an access violation) and in Robloxes case, it shuts down and makes a "An unexpected error has occoured and ROBLOX needs to quit. We're sorry!" error message.
== Disassembly and debugging ==
TODO


=== Why is there an infinite amount of exceptions when Internet Explorer opens on the studio? ===
== References ==
=== Footnotes ===
{{notelist}}


Thats because Internet Explorer sucks.
=== Citations ===
{{reflist}}

Latest revision as of 18:19, 20 November 2023

Disassembly in computers generally refers to the practice of taking apart binaries (such as .exe files) in order to analyze them without having access to the original source code. With regards to Roblox, disassembly of binaries is necessary in order to achieve any meaningful patching, such as hooks, modifying strings, and other necessary modification. There are two sections that will be covered in this article which are immediately relevant to Roblox:

  • Assembly language (ASM), a low-level programming language that all computers speak, and more specifically x86 assembly; assembly using the x86 instruction set (as Roblox clients are all x86[a])
  • Debugging and disassembly in order to find subroutine addresses to place hooks and to make other client modifications, such as cosmetic ones

Common tools used in disassembly are:

  • Cutter : A cross-platform fast disassembler with a much more welcome welcoming user interface and the Ghidra decompiler included, as well as the jsdec decompiler. Includes an experimental debugger. This is an excellent tool to get started with disassembly. This, in combination with x64dbg and Resource Hacker create a fantastic starter kit.
  • IDA Pro with Hex-Rays decompiler: An all-in-one disassembler with a debugger and decompiler (from ASM to rudimentary C++) included. This is the preferred choice for most reverse engineers as it is includes state-of-the-art disassembling technology; however, a license is necessary and prices for those can go up to more than a thousand dollars. As such, most people choose to acquire it through illegitimate means, despite it being unsafe to do so. Additionally, the learning curve is steep; newcomers may be intimidated by its complicated UI.
  • Ghidra: An alternative to IDA developed by the NSA[1]. Requires Java and has a very basic, primitive UI. However, the learning curve isn't as steep. Includes a decompiler.
  • x64dbg (Windows only): A powerful debugger for Windows. Fast, simple UI. Not too complicated to use but also not that easy. However, the learning curve is smooth and it's easy to get a hang of it.
  • HxD (Windows only): A basic hex editor for Windows. Simple UI and easy to use.
  • Resource Hacker (Windows only): A resource editor for Windows applications. Useful for editing the icon of the client, and to edit menus[b] and other special metadata.

This article assumes you have an intermediate knowledge in both C++ and computer science in general.

Assembly

Instruction set

The x86 instruction set is a vast instruction set with various extensions. Luckily, you'll only really see basic x86 instructions when debugging Roblox.

These are some common instructions (but not every instruction) that can be seen while debugging Roblox.

x86 instructions (partial list)
Instruction (NASM syntax) Name Purpose
jnz [address], jne [address] Jump if not zero, Jump if not equal The processor will set EIP to the given address if EFLAGS has the ZF (zero flag) bit cleared (generally set by a cmp operation).
jz [address], jeq [address] Jump if zero, Jump if equal The processor will set EIP to the given address if EFLAGS has the ZF (zero flag) bit set (generally set by a cmp operation).
jmp [address] Jump The processor will set EIP to the given address.
call [address] Call The processor will set EIP to the given address and then push the current address to the stack.
cmp [A], [B] Compare The processor will compare addresses A and B, and set EFLAGS to the result of the comparison.
mov [A], [B] Move The processor will set B to A.
nop No operation The processor will not do anything. This is generally only used for debugging.

Why does jne and jnz have the same purpose? Its largely for programmer readability, but cmp sets ZF in EFLAGS if the value being compared is 0 or equals the other value. jz and jeq does this too.

Where's all the data?

It may be noticed, that in the set provided above there are terms such as "EFLAGS" and "EIP". These are what are known as CPU registers. CPU registers are the fastest way to retrieve, manipulate, and store data, but are limited in size.

x86 registers (partial list)
Register Purpose
EAX General purpose register, sometimes called the Accumulator register. In C and C++ this is used for storing the return value of the last function.
EBX General purpose register, sometimes called the Base register
ECX General purpose register, sometimes used to store the loop counter. In C++, *sometimes* this points to this, the current class.
EDX General purpose register
EBP Stack Frame Pointer. This is how programs will typically safely address other values in the stack, because ESP will fluctuate wildly during execution.
ESP Stack Pointer. This is where the x86 fetches the top of the stack from. This decrements (decreases) when PUSHed to, and increments (increases) when POPed from.
EDI Destination index (typically used for arrays)
ESI Source index (typically used for arrays)
EIP Instruction Pointer. This is where the x86 fetches the next instruction from memory from, and is incremented by the size of the decoded instruction every instruction.
EFLAGS FLAGS. This is where the cmp instruction stores its results. This is not directly accessible by code itself, but can be manipulated via flow control using the jnz, jne, jeq and miscellaneous instructions.

All of the registers here are 32 bit registers, which when PUSHed take up 4 bytes in the stack.

Memory

Memory is the second fastest way to store data on x86. It is a large array of sorts, storing the program itself, all of the data the program reads and writes to, and everything else necessary for system functioning.

Values have different size depending on their storage type. Here are some storage types that are immediately relevant to us:

Type Size (in bytes) Max Value
uint64_t (unsigned long) 8 ±9,223,372,036,854,775,807 (signed), 18,446,744,073,709,551,615 (unsigned)
uint32_t (unsigned int) 4 ±2,147,483,647 (signed), 4,294,967,295 (unsigned)
uint16_t (unsigned short) 2 ±32,767 (signed), 65,535 (unsigned)
uint8_t (unsigned char) 1 ±127 (signed), 255 (unsigned)

Unsigned and Signed Integers

A signed integer is simply an integer with the last bit set to hold the "sign" bit. This bit determines if the integer is negative or positive. When it is 0, the integer is positive; and when it is 1, the integer is negative.

Integer Endianess

Endian example
Big-endian
Little-endian, the one x86 uses

In x86, integers are little endian. That means that the most significant byte (the first byte in the integer) at the largest address (the address farthest from 0), and stores the least significant byte (the last byte in the integer) at the smallest address (the address closest to 0.)

Stacks

Stacks are a form of data storage employed by most CPU architectures, including x86. In x86, the stack can be imagined as a stack of plates. You can put a plate on top of the stack (PUSH to the stack), and take the top most one off (POP from the stack).

When you call for example, the processor will PUSH the value of EIP, then go to the new address. When that subroutine eventually executes a RETurn instruction, the processor will POP the last value on the stack (which in this case, is what EIP used to be!) and then set EIP to the old address.

The stack of plates analogy breaks when it is possible to access ANY value in the stack at ANY time without POPing it, because in x86's case it has full access to the memory of the stack. This is useful when you use a calling convention, which is explained further below.

Calling Conventions

To complicate stacks even further, most programs employ a calling convention. This is most used by programming languages such as C, in order to keep track of data between functions. When functions run, without storing its initial registers the new function will overwrite them, and the data held beforehand will be lost. An example :

int x = 10;                // imagine this is a register named "x"
printf("Hello World");     // this function will likely need to use register "x" in its lifetime
printf("%i", x)           // this will print 5 as the function before overwrote register "x", thus "mangling" it

The solution to this is to use the aforementioned stack. When a function is called, it will PUSH certain registers and then CALL the address. In our basic calling convention where the callee (the thing that calls the function) stores EAX.

callee:
    mov eax, 20
    push eax
    call function
    ; eax now equals 10
    pop eax
    ; eax now equals 20
function:
    mov eax, 10
    ret

There is a catch here, for function to manipulate data. When function reaches its first instruction, this is what memory looks like at ESP for itself:

x86 stack frame
Offset Value
esp Return Address
esp + 4 bytes EAX

This is not a realistic calling convention however, as it does not store EBP (the stack frame base pointer.)

__cdecl

__cdecl is one of the calling conventions used in modern x86. It is the default calling convention used in C, and stores arguments and local variables on the stack.

Through accessing memory using ESP or EBP as offset bases the function can modify the stack, receiving arguments and allocating values on the stack.

void caller()
{
    function(5);
}

int function(int y)
{
    int x = 0;
    x = x + y;
    return x;
}

This will be assembled as:

caller:                
    push 5                ; push argument y to function
    call function
function:
    push ebp              ; preserve current frame pointer
    mov esp, ebp          ; set frame pointer to stack top
    sub esp, 4            ; allocate space for int x;
    mov eax, [ebp-4]      
    mov eax, 0            ; int x = 0
    add eax, [ebp+8]      ; x = x + y
    add esp, 4            ; restore space from int x;
    pop ebp               ; restore old frame pointer
    ret                   ; eax is the return register on __cdecl

In memory, the function will see this before executing add esp, 4:

x86 stack frame
Offset Value
esp, ebp - 4 bytes int x;
esp + 4 bytes, ebp + 0 bytes EBP
esp + 8 bytes , ebp + 4 bytes Return Address
esp + 12 bytes, ebp + 8 bytes int y;

The conventions above are using the __cdecl conventions however, and are not what you would find in some Roblox methods. Roblox and C++ classes uses a calling convention named __thiscall.

__thiscall

In __thiscall, variable arguments are not supported. However, ECX (which you might be able to recognize now) is used as this. All arguments are pushed onto the stack. A __thiscall function may look like this in ASM:

callee:
    push ecx           ; save class
    push eax           ; argument 0
    push ebx           ; argument 1
    mov ecx, class_ptr ; class
    call class_func    ; class' class_func
    add esp, 8         ; 'remove' argument 0 and 1 from stack
    pop ecx            ; restore class

This would roughly be the same as:

void caller(void)
{
    class* class = 0;
    class->class_func(0, 0);
}

The function class_func would most likely assemble to:

class_class_func:
    push ebp          ; preserve current frame pointer
    mov ebp, esp      ; create new frame pointer by pointing to the current stack top
    ret

__stdcall

__stdcall is a Win32 specific calling convention in which the callee cleans the stack. It's not very important here, so we'll brush over this.

Modules

Modules are executable files loaded into memory. When Roblox starts, Win32 will load various DLLs and run them and then run Roblox. This is why if you start Roblox with x32dbg, it will start in some random DLL instead of RobloxApp.exe for example.

Performing operations such as searching for strings will not work in modules that are not the RobloxApp module (or whatever executable Roblox is running from) since it will search memory regions that may have nothing to do with Roblox itself.

When Win32 loads a module, the module within has information where certain regions of data within the module are mapped into memory. There are 4 sections which are important:

.text

.text is where the executable code is located. It is meant to be non-writable. Most of the time you spend disassembling will be within this section.

.rodata

.rodata is where global constants are located. It is meant to be non-writable as well (Read only data.)

.data

.data is where initialized read-write data is located, such as strings.

.bss

.bss is where uninitialized read-write data is located.

Where's the stack?

The stack is not in any of these sections, and is set to an address specified by the executable upon startup.

Why does Roblox keep on stopping?

This is probably because you ran into an exception. Exceptions are used in C++ to signal when a function must quickly exit and return some error data to the parent function. For example, when trust check fails it will throw a C++ exception, which is then caught, outputting an error in the Roblox console.

I got ACCESS_VIOLATION instead!

That probably means something broke within Roblox itself. An access violation is when the program attempts to access unallocated or unusable memory, and the OS raises an error. It signals to the program that it has done something wrong (thus an access violation.) Access violations generally get passed to Roblox's crash handler.

Why is there an infinite amount of exceptions when Internet Explorer opens on old studio versions?

That's because Internet Explorer sucks tries to load a page it can't support (generally if you haven't changed the URL from http://roblox.com in AppSettings.xml.

Disassembly and debugging

TODO

References

Footnotes

  1. Until version 0.574.1.5740447 (dated 5/3/2023), which is when Roblox introduced Byfron and dropped x86 support.
  2. On MFC clients (before they fully migrated to Qt studio.)

Citations

  1. "NSA Releases Ghidra, a Free Software Reverse Engineering Toolkit." ZDNET, www.zdnet.com/article/nsa-release-ghidra-a-free-software-reverse-engineering-toolkit/.