Bismillah
When beginning to work with Assembly language, one invariably perceives a degree of difficulty owing to its low-level nature, in contrast to higher-level languages such as C or Python. In this research, we attempt to simplify assembly code so that it may be more readily understood.
The premise of this article is that we will author a simple program in C, compile it into x86 Assembly, and then explain the operations that occur in order to analyze the control flow.
Before proceeding, if you do not possess sufficient familiarity with C and Assembly, we recommend reviewing the following series: 🔗 C language series on the Shell Network 🔗 Assembly language series on the Shell Network
1. Global vs. Local Variables
Global Variables
We begin by authoring the following simple C code:
int x = 1;
int y = 2;
void main() {
x = x + y;
printf("Total = %d\n", x);
}
We then compile it using the following command:
gcc -S -masm=intel -m32 -O0 filename.c -o filename.s
When we open the file bearing the .s extension, the following is produced:
x86 Assembly:
00401003 mov eax, dword_40CF60
00401008 add eax, dword_40C000
0040100E mov dword_40CF60, eax ; [1] Store the result
00401013 mov ecx, dword_40CF60
00401019 push ecx
0040101A push offset aTotalD ; "total = %d\n"
0040101F call printf
In assembly, global variables are expressed as memory addresses (Memory Addresses) such as: dword_40CF60.
Local Variables
With respect to local variables, these are expressed as an offset relative to ebp, esp, or any other register (for example: dword ptr [ebp-4]). When we employ a disassembler such as IDA Pro (which we will examine in detail in forthcoming articles, God willing), the local variables appear clearly.
void main()
{
int x = 1;
int y = 2;
x = x+y;
printf("Total = %d\n", x);
}
x86 Assembly:
00401006 mov dword ptr [ebp-4], 1 ; [1]
0040100D mov dword ptr [ebp-8], 2 ; [2]
00401014 mov eax, [ebp-4]
00401017 add eax, [ebp-8]
0040101A mov [ebp-4], eax
0040101D mov ecx, [ebp-4]
00401020 push ecx
00401021 push offset aTotalD ; "Total = %d\n"
00401026 call printf
When we employ a disassembler such as
ida(which we will examine in forthcoming articles, God willing), the local variables appear as illustrated here.
00401006 mov [ebp+var_4], 1 ; [1]
0040100D mov [ebp+var_8], 2 ; [2]
00401014 mov eax, [ebp+var_4]
00401017 add eax, [ebp+var_8]
0040101A mov [ebp+var_4], eax
0040101D mov ecx, [ebp+var_4]
00401020 push ecx
00401021 push offset aTotalD ; "Total = %d\n"
00401026 call printf
2. If Statement
int x = 1;
int y = 2;
if(x == y) {
printf("x equals y.\n");
} else {
printf("x is not equal to y.\n");
}
x86 Assembly:
00401006 mov [ebp+var_8], 1
0040100D mov [ebp+var_4], 2
00401014 mov eax, [ebp+var_8]
00401017 cmp eax, [ebp+var_4] ; [1] Comparison instruction
0040101A jnz short loc_40102B ; [2] Conditional jump to else
0040101C push offset aXEqualsY_ ; "x equals y.\n"
00401021 call printf
00401026 add esp, 4
00401029 jmp short loc_401038 ; [3] Unconditional jump to skip else
0040102B loc_40102B: ; else section
0040102B push offset aXIsNotEqualToY ; "x is not equal to y.\n"
00401030 call printf
The first element we encounter is the CMP instruction, followed by a conditional jump. (If the conditional jump is taken, the code has followed the else path.) If it is not taken, execution continues and performs an unconditional jump, which constitutes the primary if path.
3. For Loop
int i;
for(i = 0; i < 100; i++) {
printf("i equals %d\n", i);
}
x86 Assembly:
00401004 mov [ebp+var_4], 0 ; [1] Initialization (i=0)
0040100B jmp short loc_401016 ; [2] Jump to comparison
0040100D loc_40100D: ; Update region
0040100D mov eax, [ebp+var_4] ; [3]
00401010 add eax, 1 ; Increment counter
00401013 mov [ebp+var_4], eax ; [4]
00401016 loc_401016: ; Condition region
00401016 cmp [ebp+var_4], 64h ; [5] 64h = 100
0040101A jge short loc_40102F ; [6] Conditional jump to exit loop
0040101C mov ecx, [ebp+var_4] ; Loop body
0040101F push ecx
00401020 push offset aID ; "i equals %d\n"
00401025 call printf
0040102A add esp, 8
0040102D jmp short loc_40100D ; [7] Return to update
- Initialization is performed with a local variable inside the
for. - We observe an unconditional jump that leads to the
CMPcomparison. - This is followed by a conditional jump to exit. If the exit is not taken, the instructions within the
forare executed. - Finally, an unconditional jump leads back to the update location (
incrementordecrement). - The process repeats until the exit condition is satisfied.
Figure (1): An excerpt from IDA Pro.
4. While Loop
int status = 0;
int result = 0;
while(status == 0) {
result = performAction();
status = checkResult(result);
}
x86 Assembly:
00401036 mov [ebp+var_4], 0
0040103D mov [ebp+var_8], 0
00401044 loc_401044: ; Loop start
00401044 cmp [ebp+var_4], 0 ; Comparison
00401048 jnz short loc_401063 ; [1] Conditional jump to exit
0040104A call performAction
0040104F mov [ebp+var_8], eax
00401052 mov eax, [ebp+var_8]
00401055 push eax
00401056 call checkResult
0040105B add esp, 4
0040105E mov [ebp+var_4], eax ; Update status
00401061 jmp short loc_401044 ; [2] Return to loop start
This is similar to the for loop but somewhat simpler. It begins with a CMP comparison followed by a conditional jump to exit the while. If the jump is not taken, the code inside the loop is executed, and at its conclusion an unconditional jump repeats the cycle. This continues until the CMP condition is satisfied and execution jumps out, ending the loop.
5. Switch Statement
A switch may be compiled in one of two ways depending on the compiler:
Method 1: If Style
This is nearly identical to chained if statements. We observe a number of comparisons equal to the number of cases, and after each comparison a conditional jump to execute the corresponding block. At the end, an unconditional jump leads to default.
switch(i) {
case 1: printf("i = %d", i+1); break;
case 2: printf("i = %d", i+2); break;
case 3: printf("i = %d", i+3); break;
default: break;
}
x86 Assembly:
00401013 cmp [ebp+var_8], 1
00401017 jz short loc_401027 ; [1] Case 1
00401019 cmp [ebp+var_8], 2
0040101D jz short loc_40103D ; Case 2
0040101F cmp [ebp+var_8], 3
00401023 jz short loc_401053 ; Case 3
00401025 jmp short loc_401067 ; [2] Default
00401027 loc_401027: ; Execute Case 1
00401027 mov ecx, [ebp+var_4] ; [3]
0040102A add ecx, 1
; ... (remaining instructions) ...
Figure (2): An excerpt from IDA Pro.
Method 2: Jump Table
switch(i) {
case 1: printf("i = %d", i+1); break;
case 2: printf("i = %d", i+2); break;
case 3: printf("i = %d", i+3); break;
case 4: printf("i = %d", i+4); break;
default: break;
}
x86 Assembly:
00401016 sub ecx, 1 ; Subtract 1 because the compiler starts from 0
00401019 mov [ebp+var_8], ecx
0040101C cmp [ebp+var_8], 3 ; Compare against maximum
00401020 ja short loc_401082 ; If out of range, go to default
00401022 mov edx, [ebp+var_8]
00401025 jmp ds:off_401088[edx*4] ; [1] Direct jump via the table
; --- Target addresses ---
0040102C loc_40102C:
; ...
00401042 loc_401042:
; ...
00401082 loc_401082: ; End (Default/Exit)
00401082 xor eax, eax
00401087 retn
; --- Jump table ---
00401088 off_401088: ; [2]
00401088 dd offset loc_40102C
0040108C dd offset loc_401042
00401090 dd offset loc_401058
00401094 dd offset loc_40106E
The jump-table method relies on subtracting a value to obtain a zero-based index, then comparing it against the number of cases. If it falls within range, it jumps directly using the table according to the equation [edx*4]. If it is outside the range, default is executed immediately.
Figure (3): An excerpt from IDA Pro.
6. Arrays
int b[5] = {123, 87, 487, 7, 978};
void main() {
int i;
int a[5];
for(i = 0; i < 5; i++) {
a[i] = i;
b[i] = i;
}
}
x86 Assembly:
00401021 mov edx, [ebp+var_18]
00401024 mov [ebp+ecx*4+var_14], edx ; [1] Local Array
00401028 mov eax, [ebp+var_18]
0040102B mov ecx, [ebp+var_18]
0040102E mov dword_40A000[ecx*4], eax ; [2] Global Array
The memory address of an array depends on its declaration (global or local). It is always accompanied by a register acting as an index multiplied by the size of its elements. (For example: for an integer array the index is multiplied by 4, expressed as [ecx * 4].) Note that array indices start at 0, so the last element is at index n-1.
7. Structs & Linked Lists
Struct
struct my_structure { ; [1]
int x[5];
char y;
double z;
};
struct my_structure *gms; ; [2]
void main() {
gms = (struct my_structure *) malloc(sizeof(struct my_structure));
test(gms);
}
x86 Assembly:
00401053 push 20h ; Struct size (32 bytes)
00401055 call malloc
0040105A add esp, 4
0040105D mov dword_40EA30, eax ; Store base address
00401062 mov eax, dword_40EA30
00401067 push eax ; [1] Pass pointer to function
00401068 call sub_401000
A struct is declared as a variable, and values are written into it according to the variables it contains and according to the function call. When declaring the struct and allocating space for it with malloc, we are provided with the base address, which marks the beginning of the struct.
Linked List
struct node {
int x;
struct node * next;
};
typedef struct node pnode;
void main() {
pnode * curr, * head;
int i;
head = NULL;
for(i=1; i<=10; i++) { ; [1] Build the nodes
curr = (pnode *)malloc(sizeof(pnode));
curr->x = i;
curr->next = head;
head = curr;
}
curr = head;
while(curr) { ; [2] Traverse the nodes
printf("%d\n", curr->x);
curr = curr->next;
}
}
x86 Assembly:
0040107E mov [esp+18h+var_18], 8 ; Node size
00401085 call malloc
0040108A mov [ebp+var_4], eax ; eax holds the new node address
0040108D mov edx, [ebp+var_4]
00401090 mov eax, [ebp+var_C]
00401093 mov [edx], eax ; [1] curr->x = i
00401095 mov edx, [ebp+var_4]
00401098 mov eax, [ebp+var_8]
0040109B mov [edx+4], eax ; [2] curr->next = head
0040109E mov eax, [ebp+var_4]
004010A1 mov [ebp+var_8], eax ; head = curr
The fundamental difference between a conventional struct and a linked list in assembly is the profusion of mov operations that indicate the construction of the links between nodes, as shown in the lines marked [1] and [2].