Assembly
Assembly
The Basics
The Working of the CPU
A computer works by having 3 primary components:
- CPU
- Memory
- Input/Output devices
These three components are connected with each other using buses, they transmit all the data between these components.
The way it works, is that when we submit input (using an I/O device), it’s transmitted to the memory using buses, then this data is moved to the CPU for processing, and when it’s finished processing, it’ll move back to the memory using buses, and back again to the I/O device to, for example, be displayed on the screen.
The CPU is made of different components:
- Control unit: The main purpose of this component is to control what is
happening at any time within the processor. Its job is to perform 4 basic
functions:
- Retrieve instructions from memory.
- Decode instructions for operation.
- Retrieve data from memory as needed.
- Store the reserves as necessary.
- Execution Unit: The main function of the processor is to execute instructions, this function is performed in the execution unit.
- Internal memory locations: The CPU has internal memory locations called registers, which are interconnected with the execution unit. These registers are capable of storing data elements for processing without having to access the memory storage unit. There are some special kinds of registers called flags which are used to perform status checks on each arithmetic and logical operations in the CPU.
-
Registers of the CPU
A register is a small memory space in the CPU to hold data for processing instructions. They can store any CPU instruction, any memory address, or any kind of data.
As we’ll be focusing on 32-bit assembly, the size of the registers are 32-bit, 16-bit, and 8-bit.
The lower 8-bit registers are called accumulator, base, count, and data, denoted by a, b, c and d respectively.
If we add 8 more bits to our 8-bit registers, we’ll have 16-bit registers: AX, BX, CX, and DX.
If we add 16 more bits to our 16-bit registers, they are now called extended registers and their names remain the same, but now have an E at the beginning of them: EAX, EBX, ECX, EDX.
+------+ +--------------+--------------+ |32 bit+-----------------------+ 16 bit | 16 bit | +------+ +--------------+-------+------+ | | +--------+--------+ | 8 bit | 8 bit | /--------+--------\ / \ / \ / \ Higher bits Lower bitsIf we take for example the EAX register:
AX AX +------+ +--------------+--------------+ | EAX +-----------------------+ 16 bit | 16 bit | +------+ +--------------+-------+------+ | AH | AL +--------+--------+ | 8 bit | 8 bit | /--------+--------\ / \ / \ / \ Higher bits Lower bitsThe lower 8-bits are called AL, and the higher 8 bits are called AH. If we combine AH and AL we get AX.
There are three types of registers in the CPU:
- General Purpose: We have 8 32-bit registers that are used for storing working
data:
- EAX: Accumulator (space) for operands and results data.
- EBX: Pointer to data in the data memory segment.
- ECX: Counter for string and loop operations.
- EDX: I/O Pointer.
- EDI: Data pointer for destination index of string operations.
- ESI: Data pointer for source index of string operations.
- ESP: Stack pointer.
- EBP: Stack base pointer.
- Segment: There are 6 segment registers that are used for referencing memory
locations:
- CS: Code segment contains the pointer to the code segment in memory. The code segment is where the instruction codes are stored in memory.
- DS, ES, FS, GS: Data segment pointer
- SS: Stack segment, points to the stack area in memory.
- Instruction Pointer: Called the program counter, it tells the computer where to go next to execute the next commands and controls the flow of the program.
- General Purpose: We have 8 32-bit registers that are used for storing working
data:
-
Flags
To understand the concept of a flag, let’s take a simple example (write an example of using the status flag for a simple addition or other operation.)
(write examples for each flag defined)
In 32-bit CPUs, there’s a 32-bit register where the flags are stored. They are a collection of binary indiactors (meaning each one takes only one bit) that represent the current state of the processor. Some of the status flags are:
- Carry Flag.
- Zero Flag.
- Auxiliary Flag.
- Sign Flag.
- Overflow Flag.
- Parity Flag.
These flags are used by the CPU to make decisions during the execution of the program, such as whether to jump to a different part of the program, or whether to continue with the current operation.
After the status flags, we have system flags, they are meant to control the overall behaviour of the CPU and its interaction with the system. They are usually set by the operating system rather than individual programs.
After system flags we have control flags. They are used to control specific behaviour in the processor. There’s only one flag defined: DF (direction flag), it’s used to control the way the strings are handled by the processor.
The remaining flags are reserved for the system itself.
Memory Layout of a Program
Whenever we write a program, all the data is stored according to this memory layout:
+----------------------------------------------------+
| Command line arguments and environment variables |
+----------------------------------------------------+
| |
| Stack |
| |
+-------------------------+--------------------------+
| | |
| | |
| Space to grow |
| | |
| | |
+-------------------------+--------------------------+
| |
| Heap |
| |
+----------------------------------------------------+
| Uninitialised data .bss |
+----------------------------------------------------+
| Initialised data .data |
+----------------------------------------------------+
| .text |
| executable code instructions |
+----------------------------------------------------+
In the first segment (going top-down), we have some space reserved for command line arguments and environment variables.
After that we have the stack, which is our function data.
Then we have the heap, which is dynamic memory, where all the data is stored.
Then we have our BSS segment, which is where uninitialised data is stored.
After that, we have our DATA segment, which is where our initialised data will be stored.
And finally, we have the TEXT segment, which are just all the executable code instructions of the program.
Let’s look at a small C program to understand all these segments:
int var1;
int var2 = 4;
int
main (void)
{
return 0;
}
The variable var1 will be in our BSS segment, as it’s uninitialised. var2 will
be in our DATA segment as it’s initialised. The data used in our main function
will go to our stack (all the variables we declare inside of it). All the
executable code of our program (which will just be the contents inside of the
main function) are going to be in the text segment.
Writing Hello World in Assembly
The Structure of an Assembly Program
To code in assembly, we need to create a file ending in *.s, for example: “hello_world.s”.
First, let’s define the sections that our program will use:
.section .data
In the .data section, we’ll define our initialised variables. After that, we’ll define the .bss section for the uninitialised data:
.section .data
.section .bss
Then, we’ll define the text segment:
.section .data
.section .bss
.section .text
Where the actual instructions of our code will be stored.
Then, we’ll need to define our main function (as we do with C), using .globl and providing a label name, and defining that label:
.section .data
.section .bss
.section .text
.globl _start
_start:
To compile an assembly program there are two steps we need to follow: first, we need to assemble it using the as utility:
as hello_world.s -o program.o
With the -o flag we are specifying the name of the output object file that will
be generated (hence the *.o extension). Once the program is assembled, we need
to link it using ld:
ld program.o -o program.out
The syntax is pretty much the same, we specify the object files that we want to link, and then we specify a name for the output.
Once we have the output file program.out if we try to run it we’d run into an error, it can be either a message stating that we cannot execute a binary file, or we’d get a segmentation fault (which is what is called undefined behaviour), but that happens only because our program doesn’t have any instructions, and it’s not gracefully exiting.
System Calls
In an operating system there are some pre-built tasks which are called system calls, they are useful to allow programs to perform operations that are reserved (or implemented) at the operating system level, for example, creating files, outputting to a file, allocating memory, etc.
For example, if we have this very simple C program:
#include <stdio.h>
int
main (void)
{
printf ("Hello, World!\n");
return 0;
}
The way that it works to print out something to the screen is using the write
system call, which is used to write data into a file.
In Linux there are 3 pre-defined streams that allow us to manage data:
stdinis the keyboard input.stdoutis the output to the screen.stderris for outputting errors.
Each of these files have an unique ID that is constant between all programs: for
stdin it’s 0, for stdout it’s 1, and for stderr it’s 2.
So to write something onto the screen, we need to write into the file descriptor 1 (as it’s the file descriptor for the stdout).
Hello World in Assembly
In order to write hello world in assembly to the screen we need to know two things:
- What is the system call number for the write syscall.
- What are the arguments required for this system call.
To know the system call number that we need to call, we can do so reading the
contents of the file located in /usr/include/asm/unistd_32.h. In that file,
we’ll find the following line:
#define __NR_write 4
Meaning that the system call number for write is 4.
For now, that’s enough for us to start writing our assembly program, so we’ll start from the basic structure of a program:
.section .data
.section .bss
.section .text
.globl _start
_start:
In the data section, we’ll define a msg variable that will hold the value of the string we want to print, which in this case is “Hello, World!\n”. In assembly we define variables as labels, so we can just do:
msg:
.ascii "Hello, World\n"
In the msg label, we’ve defined data of type ascii using .ascii with a value of
“Hello, World\n”.
Now, in our _start label we need to specify which system call we are using. For
that, we are going to use the mov instruction:
movl $4, %eax # syscall number
What the mov instruction does is to move data from the source to the
destination, in our case our source is 4, and our destination is the eax
register (as this one must hold the syscall number we want to call). Notice that
we need to use the $ sign with number literals, and % with registers.
Once we are finished specifying the syscall number we want to call, we need to specify the other arguments for the systemcal call we are intending to use. The write system call takes three arguments:
- The file descriptor into ebx.
- The address of the message that we want to write into the specified file into ecx.
- The length of our message into ecx.
After specifying those parameters we’ll have a _start label similar to this one:
_start:
movl $4, %eax # syscall number
movl $1, %ebx # file descriptor to write to
movl $msg, %ecx # pointer to our message
movl $13, %edx # buffer size in bytes
Finally, once we are finished specifying the parameters, we are ready now to
actually do the system call. We do so with the int keyword and specify the
number of the system call we want to execute. The write system call is part of
the 0x80 system call, so we’d specify it like this:
int $0x80
Now, we can assemble, link and execute our program like this:
as -32 hello_world.s -o hello_world.o
ld -m elf_i386 hello_world.o -o hello_world.out
./hello_world.out
And we’d get an output similar to this one:
Hello, World
segmentation fault (core dumped)
Again, we are having the segmentation fault we had earlier. As explained before,
this happens because we are not exiting gracefully the program, to do so we have
another syscall which is the exit system call.
To implement this exit systemcall, refer back to the unistd_32.h file:
#define __NR_exit 1
So now, we know that the exit syscall is 0. And to call it from our assembly
program, we also need to specify an exit status code:
movl $1, %eax
movl $0, %ebx
int $0x80
This is how our entire program looks like:
.section .data
msg:
.ascii "Hello, World\n"
.section .bss
.section .text
.globl _start
_start:
movl $4, %eax
movl $1, %ebx
movl $msg, %ecx
movl $13, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
Debugging our Program
To debug our program we’ll use the GNU Debugger (GBD). To use it, we need to first assemble and link our program. Once that’s done, we can launch it like this:
gdb ./program.out
After we execute GDB, we’ll get a different prompt where we can input different commands:
(gdb) commands go here
What we need to do first, is to disassemble our _start label:
(gdb) disassemble _start
And we’ll get an output similar to this:
Dump of assembler code for function _start:
0x0000000000401000 <+0>: mov $0x4,%eax
0x0000000000401005 <+5>: mov $0x1,%ebx
0x000000000040100a <+10>: mov $0x402000,%ecx
0x000000000040100f <+15>: mov $0xd,%edx
0x0000000000401014 <+20>: int $0x80
0x0000000000401016 <+22>: mov $0x1,%eax
0x000000000040101b <+27>: mov $0x0,%ebx
0x0000000000401020 <+32>: int $0x80
End of assembler dump.
Which is an identical replica of our code (with the difference of msg being its
address, instead of the name of the label).
To execute our code step by step we can place a breakpoint in our _start label
by doing:
(gdb) break * _start
Notice that we can use the address that the disassemble output returned to us, instead of specifying _start we could also use 0x0000000000401000.
To run our program (so it stops at the breakpoint), we can use the run command:
(gdb) run
Breakpoint 1, 0x0000000000401000 in _start ()
As our program stopped at the movl $4, %eax it hasn’t been executed yet, and
will be the next instruction to be executed once the program finishes
running. We can confirm that by running:
(gdb) info registers
rax 0x0 0
rbx 0x0 0
rcx 0x0 0
rdx 0x0 0
rsi 0x0 0
rdi 0x0 0
rbp 0x0 0x0
rsp 0x7fffffffdcd0 0x7fffffffdcd0
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
r13 0x0 0
r14 0x0 0
r15 0x0 0
rip 0x401000 0x401000 <_start>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
pl3_ssp <unavailable>
fs_base 0x0 0
gs_base 0x0 0
Even though my operating system is 64 bits, the equivalent of eax is rax and the
same with the other registers. We can see that its value is 0.
To execute our next instruction we can use the ni command (which stands for next
instruction):
(gdb) ni
0x0000000000401005 in _start ()
If we execute info registers again:
rax 0x4 4
If we run ni again, then the instruction where we move 1 into ebx will be
executed, changing the value of ebx (which can be confirmed with info registers). If we do it again, then we’ll be at the instruction where we move
the address of our message into ecx. When we execute info registers:
rcx 0x402000 4202496
We can see the address of our message. To view the contents of that address, we
can usee the x/s command in GDB (which is used to print strings) like this:
(gdb) x/s 0x402000
0x402000: "Hello, World\n"
If we want to print a single character at an address, instead of the entire
string, we can use the x/c command instead. If we execute ni one more time, then
the value of the edx register will change as well.
The next instruction after we move the length of our message into the edx
register is to execute an interrupt to trigger a system call, if we execute ni
one more time, this will be the output:
(gdb) ni
Hello, World
0x0000000000401016 in _start ()
If we keep going instruction per instruction, we’ll reach the exit point of our program, and we’ll get the following output:
(gdb) ni
[Inferior 1 (process 8974) exited normally]
Using C Functions in Assembly
We can use C functions from assembly, let’s try to use the printf function in a
new program. We’ll start it with the basic structure:
.section .data
msg:
.ascii "Hello, World!\n"
.section .text
.globl _start
_start:
In the _start label we must first push the data into the stack (in our case our
msg label), and then call the printf function:
_start:
pushl $msg
call printf
And after that, we need to exit our program. We can do it using the exit function by pushing its argument into the stack as well:
pushl $0
call exit
Now we need to assemble it and link it:
as program.s -o program.o
ld -lc -dynamic-linker /lib/ld-linux.so.2 program.o -o program.out
If it doesn’t work, we can rename our _start label to main, and compile it using GCC:
gcc program.s -o program.out
If we are using a 64 bit operating system, we must specify the -m32 and -no-pie flags:
gcc -m32 -no-pie program.s -o program.out
Moving Data
Defining Data
In assembly there are different datatypes we can use:
| Directive | Data Type |
|---|---|
| .ascii | Text string |
| .asciz | Null-terminated text string |
| .byte | Byte value |
| .double | Double-precision floating-point number |
| .float | Single-precision floating-point number |
| .int | 32-bit integer number |
| .long | 32-bit integer number (same as .int) |
| .octa | 16-byte integer number |
| .quad | 8-byte integer number |
| .short | 16-bit integer number |
| .single | Single-precision floating-point number (same as float) |
To define variables in C, for example, we do it this way:
char *string = "Hello, World!\n";
int a = 3;
float pi = 3.14159;
To define those same variables in assembly, we would do it this way:
.section .data
string:
.ascii "Hello, World!\n"
a:
.int 3
pi:
.float 3.14159
Using static symbols
Consider the following C program:
#include <stdio.h>
#define MSG "Hello, World!\n"
int
main (void)
{
printf (MSG);
return 0;
}
Now, with a basic assembly program:
.section .data
_msg:
.ascii "Hello, World\n"
.section .text
.globl _start
_start:
movl $4, %eax
movl $1, %ebx
movl $msg, %ecx
movl $13, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
To declare a static symbol (to replace the 13 which is the length of our
message), in the .data section, we can use the .equ directive to create a static
symbol:
.section .data
.equ STRING_LEN, 13
And then we can use it like this:
movl $STRING_LEN, %edx
Defining data in .bss
Let’s start with a basic program:
.section .bss
.section .text
.globl _start
_start:
To define data in our .bss section we need the .comm directive, it means Common
Memory Area, and we use it like this:
.section .bss
.comm buffer, 16
In that line, we are creating a variable called buffer, with a size in bytes of 16.
Now, with a variable created, in our _start label we will read input from the
user, then we will write the contents to the screen, and finally we will exit
the program:
.section .bss
.comm buffer, 16
.section .text
.globl _start
_start:
# read syscall
# write syscall
# exit syscall
To know what input the read syscall, we can execute man 2 read and we’ll get a
man page, with a function definition like this:
ssize_t read(int fd, void buf[count], size_t count);
The first input is the file descriptor, then the buffer pointer, and finally the
buffer size, which in this case is 16 bytes. And finally, to get the syscall
number we can again check the file located at /usr/include/asm/unistd_32.h.
# ..._start
movl $3, %eax # read syscall number
movl $0, %ebx # stdin fd
movl $buffer, %ecx # buf
movl $16, %edx # count
int $0x80
Now that we got the input from the user into the buffer variable, we can now
print it out to the screen, and then exit our program:
# [...]
movl $4, %eax
movl $1, %ebx
movl $buffer, %ecx
movl $16, %edx
int $0x80
# exit the program
movl $1, %eax
movl $0, %ebx
int $0x80
Moving Data
In assembly there are different instructions for moving data, they have the form
of movX, with X being the size of the data we are moving. For example:
movl- moving 32-bit long data.movw- moving 16-bit long data.movb- moving 8-bit long data
And the instruction follows this form:
movX src, dst
For example:
movl $4, %eax
That instruction will set the value of the eax register to 4.
For example:
.section .text
.globl _start
_start:
movl $190, %eax
movw $3, %bx
movb $1, %cl
We can also use it to set the values of variables:
.section .bss
.comm buffer, 8
.section .text
.globl _start
_start:
movl $100, %buffer # set buffer value of 100
movl buffer, %ecx # set ecx with the value of buffer
movl $buffer, %edx # set edx with the value of buffer (with the $ symbol we move the address of the data)
movl $500, %eax # set eax to 500
movl %eax, (%ebx) # set the value of eax to the address that is inside of ebx, changing the value of buffer
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
Accessing and Moving Indexed Values
Let’s see how to create, and use arrays in assembly.
As usual, let’s start with our basic program structure, and we’ll create a label
in our data section which will hold an initialised array like this:
.section .data
numbers:
.int 10, 20, 30, 40, 50, 60
.section .text
.globl _start
_start:
# program goes here
We can define labels that contain multiple elements like that, just by separating them with a comma.
To access these elements we must use a formula, in which we need to specify the base address, the offset address, the index, and the size of each individual element (in bytes). We can do it like this:
_start:
# base_address (offset_address, index, size)
# numbers(, 2, 4)
movl $2, %edi # move the index into the edi register (it should be moved to the edi register)
movl numbers(, %edi, 4), %eax # move 30 into the %eax register
# exit the program
movl $1, %eax
movl $0, %ebx
int $0x80
In our specific case, the offset address would be zero, so we can skip it by not
specifying a number. For this example, we will be accessing the number 30 so its
index would be 2 (as they are counted from zero), and as we know the size of an
integer is 4 bytes, so we specify it as its size.
Now let’s check with GDB if the number 30 is indeed inside of the eax register. After we assemble, and link our program, we need to run GDB like this:
gdb ./arrays.out
Let’s get started by disassembling the program so we can get the address of our
_start label and set a breakpoint where we move the number 30 into the eax
register (our second instruction):
(gdb) disassemble _start
Dump of assembler code for function _start:
0x08049156 <+0>: mov $0x2,%edi
0x0804915b <+5>: mov 0x804c00c(,%edi,4),%eax
0x08049162 <+12>: mov $0x1,%eax
0x08049167 <+17>: mov $0x0,%ebx
0x0804916c <+22>: int $0x80
End of assembler dump.
(gdb) break * 0x0804915b
Breakpoint 1 at 0x804915b
(gdb) run
Breakpoint 1, 0x0804915b in _start ()
By the time when we reach our move instruction to the eax register, the move to
the edi must’ve happened, so we can check it by using info registers:
(gdb) info registers
[...]
edi 0x2 2
We can see the values of our array using the examine command like this: x/Nd &numbers, where N represents the number of elements we want to see, as our array
has 6 elements, we would run:
(gdb) x/6d &numbers
0x804c00c: 10 20 30 40
0x804c01c: 50 60
So, in our next instruction 30 must be moved into the eax register. We can move
to the next instruction using ni and check the registers again:
(gdb) ni
0x08049162 in main ()
(gdb) info registers
eax 0x1e 30
Direct and Indirect Addressing
Let’s consider the following to be our memory region:
+-------------------------+
| |
| 0xAAAA |
| |
+-------------------------+
| |
| 0xBBBB |
| |
+-------------------------+
| |
| 0xCCCC |
| |
+-------------------------+
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
+-------------------------+
Suppose we have three memory addresses: 0xAAAA, 0xBBBB, and 0xCCCC. If, for
example, we want to move the value 5 into the memory address 0xAAAA, we can do
it by just specifying that address like this:
movl $5, 0xAAAA
In that instruction we are moving the value 5 directly into the address 0xAAAA, that’s called direct memory access.
If we cannot access the memory address 0xAAAA, meaning we cannot access that
address directly, in that case we’ll perform two steps to move 5 into that
memory address. The first step would be to move that memory address into one of
our general purpose registers:
movl $0xAAAA, %eax
%eax now contains a pointer to that address. And the last step would be to move the 5 into the address that %eax points to like this:
movl $5, (%eax)
Whenever we move memory indirectly we need to enclose the register in brackets, meaning that the register contains a pointer. That is called indirect memory addressing.
An entire program using indirect memory addressing would look like this:
.section .data
number:
.int 0
.section .text
.globl _start
_start:
# direct addressing
movl $5, number
# indirect addressing
movl $number, %eax # move address to %eax (we use $ to move the memory address)
movl $10, (%eax)
Let’s debug our program to make sure that it works (first assemble it and link it).
(gdb) disassemble start
Dump of assembler code for function start:
0x08049156 <+0>: movl $0x5,0x804c00c
0x08049160 <+10>: mov $0x804c00c,%eax
0x08049165 <+15>: movl $0xa,(%eax)
End of assembler dump.
(gdb) break * 0x08049156
Breakpoint 1 at 0x8049156
(gdb) run
Breakpoint 1, 0x08049156 in start ()
(gdb) ni
0x08049160 in start ()
(gdb) x/x &numbers
0x804c00c: 0x00000005
(gdb) ni
0x08049165 in start ()
(gdb) info reg $eax
eax 0x804c00c 134529036
(gdb) ni
0x0804916b in ?? ()
(gdb) x/d &numbers
0x804c00c: 10
(gdb)
Indirect Address Pointer
Consider the following program:
.section .data
numbers:
.int 4, 8
.section .text
.globl _start
_start:
movl $numbers, %eax
movl $2, (%eax)
movb $9, 2(%eax)
movw $3, 6(%eax)
There we have a variable numbers with two integer values: 4, and 8. Meaning that
it’s a total of 8 bytes in size.
It’ll be represented in memory like this:
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 8 | 0 | 0 | 0 | 4 |
+---+---+---+---+---+---+---+---+
7 6 5 4 3 2 1 0
In the _start label, we are moving the memory address of numbers into the %eax
register. In our next instruction we are moving 4 bytes (containing 2) into the
address stored in the %eax register. And now it’ll be represented in memory like
this:
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 8 | 0 | 0 | 0 | 2 |
+---+---+---+---+---+---+---+---+
7 6 5 4 3 2 1 0
In our next instruction, where we are moving a single byte using movb, we’ll
move a single byte containing 9 into the second index of the specified
address. Notice that as %eax points to the base address (meaning byte 0), the
result of that operation will result in:
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 8 | 0 | 9 | 0 | 2 |
+---+---+---+---+---+---+---+---+
7 6 5 4 3 2 1 0
Only the second index will be changed, as we are moving a single byte.
In the next instruction, we are moving two bytes as we used movw, it’ll move two
bytes containing the value 3 into the sixth byte of our variable, resulting in
the following memory layout:
+---+---+---+---+---+---+---+---+
| 0 | 3 | 0 | 8 | 0 | 9 | 0 | 2 |
+---+---+---+---+---+---+---+---+
7 6 5 4 3 2 1 0
Creating Stack Frames
Let’s start with a basic assembly program:
.section .text
.globl _start
_start:
# create stack frame
movl %esp, %ebp # copy the esp address to ebp
subl $8, %esp # create 8 bytes in memory in our stack frame (subtrack means move upwards)
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
Now we can check it using GDB:
(gdb) disassemble _start
Dump of assembler code for function _start:
0x08049156 <+0>: mov %esp,%ebp
0x08049158 <+2>: sub $0x8,%esp
0x0804915b <+5>: mov $0x1,%eax
0x08049160 <+10>: mov $0x0,%ebx
0x08049165 <+15>: int $0x80
End of assembler dump.
(gdb) b * 0x08049156
Breakpoint 1 at 0x8049156
(gdb) run
Breakpoint 1, 0x08049156 in main ()
(gdb) ni
0x08049158 in _start ()
(gdb) info registers
esp 0xffffcdec 0xffffcdec
ebp 0xffffcdec 0xffffcdec
(gdb) p/d $ebp-$esp
$1 = 0 # they point to the same address (this will get the size of our stack frame)
(gdb) ni
0x0804915b in _start ()
(gdb) info registers
esp 0xffffcde4 0xffffcde4
ebp 0xffffcdec 0xffffcdec
(gdb) x/8bx $esp
0xffffcde4: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(gdb)
Before we start adding and removing data to our stack, let’s review something first. Let’s consider the following is our stack:
+------------------------------<----------> ESP
| |
| |
| |
+------------------------------+
| |
| |
| |
+------------------------------+
| |
| |
| |
+------------------------------+
| |
| |
| |
+------------------------------+
| |
| |
| |
+------------------------------+
| |
| |
| |
+------------------------------+
| |
| |
| |
+------------------------------+---------> EBP
To add something into the stack, we’ll need to use the push
instruction. Whenever we add data to our stack, the esp register will point at
the top of it (the latest item added to the stack).
To remove data from the stack, we’ll use the pop instruction. Let’s continue
from the previous program:
.section .text
.globl _start
_start:
# create stack frame
movl %esp, %ebp # copy the esp address to ebp
subl $8, %esp # create 8 bytes in memory in our stack frame (subtrack means move upwards)
# add data into the stack frame
movl $128, %eax # to push data, it must be in a register
pushl %eax
movl $64, %ebx # add more data, 8 bytes that were allocated are used now
pushl %ebx
# remove data from stack
popl %ebx
popl %eax
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
Data Exchange Instructions
Let’s get started with a simple program:
.section .text
.globl _start
_start:
movl $3, %eax
movl $5, %ebx
xchg %eax, %ebx # exchange data between eax and ebx
movl $0x12345678, %ecx
bswap %ecx # swap the order of the bytes in %ecx
# exit program
movl $1, %eax
movl $0, %ebx
int $0x80
The xchg instruction will set the value of %eax to the value of %ebx and
vice-versa.
And bswap will swap the order of the bytes in the register that we specify.
Setting and Clearing the Carry Flag
Let’s see how we can enable and disable flags. Let’s consider the following program:
.section .text
.globl _start
_start:
# set the carry flag (CF)
stc # Set Carry Flag
# clear carry flag (CF)
clc # CLear Carry
# exit program
movl $1, %eax
movl $0, %ebx
int $0x80
And now we can check it with GDB:
(gdb) disassemble main
Dump of assembler code for function main:
0x08049156 <+0>: stc
0x08049157 <+1>: clc
0x08049158 <+2>: mov $0x1,%eax
0x0804915d <+7>: mov $0x0,%ebx
0x08049162 <+12>: int $0x80
End of assembler dump.
(gdb) b * 0x08049156
Breakpoint 1 at 0x8049156
(gdb) display /5i $eip
(gdb) display / $eflags
(gdb) run
Breakpoint 1, 0x08049156 in main ()
1: x/5i $eip
=> 0x8049156 <main>: stc
0x8049157 <main+1>: clc
0x8049158 <main+2>: mov $0x1,%eax
0x804915d <main+7>: mov $0x0,%ebx
0x8049162 <main+12>: int $0x80
2: $eflags = [ PF ZF IF ]
(gdb) (gdb) ni
0x08049157 in main ()
1: x/5i $eip
=> 0x8049157 <main+1>: clc
0x8049158 <main+2>: mov $0x1,%eax
0x804915d <main+7>: mov $0x0,%ebx
0x8049162 <main+12>: int $0x80
0x8049164 <_fini>: push %ebx
2: $eflags = [ CF PF ZF IF ]
(gdb) ni
0x08049158 in main ()
1: x/5i $eip
=> 0x8049158 <main+2>: mov $0x1,%eax
0x804915d <main+7>: mov $0x0,%ebx
0x8049162 <main+12>: int $0x80
0x8049164 <_fini>: push %ebx
0x8049165 <_fini+1>: sub $0x8,%esp
2: $eflags = [ PF ZF IF ]
(gdb)
Setting and Clearing the Overflow Flag in assembly
Consider the following program:
.section _text
.globl _start
_start:
# enable overflow flag (OF)
movl $0x7fffffff, %eax
addl $1, %eax # overflow register
# clear OF flag
xorl %eax, %eax # remove data from eax
# exit program
movl $1, %eax
movl $0, %ebx
int $0x80
And now let’s check it on GDB:
(gdb) disassemble main
Dump of assembler code for function main:
0x08049156 <+0>: mov $0x7fffffff,%eax
0x0804915b <+5>: add $0x1,%eax
0x0804915e <+8>: xor %eax,%eax
0x08049160 <+10>: mov $0x1,%eax
0x08049165 <+15>: mov $0x0,%ebx
0x0804916a <+20>: int $0x80
End of assembler dump.
(gdb) b * 0x08049156
Breakpoint 1 at 0x8049156
(gdb) display /4i $eip
(gdb) display / $eflags
(gdb) run
Breakpoint 1, 0x08049156 in main ()
1: x/4i $eip
=> 0x8049156 <main>: mov $0x7fffffff,%eax
0x804915b <main+5>: add $0x1,%eax
0x804915e <main+8>: xor %eax,%eax
0x8049160 <main+10>: mov $0x1,%eax
2: $eflags = [ PF ZF IF ]
(gdb) set $eflags=0x0
(gdb) ni
0x0804915b in main ()
1: x/4i $eip
=> 0x804915b <main+5>: add $0x1,%eax
0x804915e <main+8>: xor %eax,%eax
0x8049160 <main+10>: mov $0x1,%eax
0x8049165 <main+15>: mov $0x0,%ebx
2: $eflags = [ IF ]
(gdb) ni
0x0804915e in main ()
1: x/4i $eip
=> 0x804915e <main+8>: xor %eax,%eax
0x8049160 <main+10>: mov $0x1,%eax
0x8049165 <main+15>: mov $0x0,%ebx
0x804916a <main+20>: int $0x80
2: $eflags = [ PF AF SF IF OF ]
(gdb) i r eax
eax 0x80000000 -2147483648
(gdb) ni
0x08049160 in main ()
1: x/4i $eip
=> 0x8049160 <main+10>: mov $0x1,%eax
0x8049165 <main+15>: mov $0x0,%ebx
0x804916a <main+20>: int $0x80
0x804916c <_fini>: push %ebx
2: $eflags = [ PF ZF IF ]
(gdb) c
Setting and clearing the Parity Flag
Consider this program:
.section .text
.globl _start
_start:
# enable parity flag (PF)
movb $0xAA, %al # 0xAA = 0b10101010
test %al, %al # AND
# clear PF
movb $0x57, %al
test %al, %al
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
Now, we can check it in GDB:
(gdb) disassemble main
Dump of assembler code for function main:
0x08048336 <+0>: mov $0xaa,%al
0x08048338 <+2>: test %al,%al
0x0804833a <+4>: mov $0x57,%al
0x0804833c <+6>: test %al,%al
0x0804833e <+8>: mov $0x1,%eax
0x08048343 <+13>: mov $0x0,%ebx
0x08048348 <+18>: int $0x80
End of assembler dump.
(gdb) b * 0x08048336
Breakpoint 1 at 0x8048336
(gdb) display / $eflags
1: $eflags = <error: No registers.>
(gdb) display /4i $eip
2: x/4i $eip
<error: No registers.>
(gdb) run
Breakpoint 1, 0x08048336 in main ()
1: $eflags = [ PF ZF IF ]
2: x/4i $eip
=> 0x8048336 <main>: mov $0xaa,%al
0x8048338 <main+2>: test %al,%al
0x804833a <main+4>: mov $0x57,%al
0x804833c <main+6>: test %al,%al
(gdb) set $eflags=0x0
(gdb) ni
0x08048338 in main ()
1: $eflags = [ IF ]
2: x/4i $eip
=> 0x8048338 <main+2>: test %al,%al
0x804833a <main+4>: mov $0x57,%al
0x804833c <main+6>: test %al,%al
0x804833e <main+8>: mov $0x1,%eax
(gdb) ni
0x0804833a in main ()
1: $eflags = [ PF SF IF ]
2: x/4i $eip
=> 0x804833a <main+4>: mov $0x57,%al
0x804833c <main+6>: test %al,%al
0x804833e <main+8>: mov $0x1,%eax
0x8048343 <main+13>: mov $0x0,%ebx
(gdb) ni
0x0804833c in main ()
1: $eflags = [ PF SF IF ]
2: x/4i $eip
=> 0x804833c <main+6>: test %al,%al
0x804833e <main+8>: mov $0x1,%eax
0x8048343 <main+13>: mov $0x0,%ebx
0x8048348 <main+18>: int $0x80
(gdb) ni
0x0804833e in main ()
1: $eflags = [ IF ]
2: x/4i $eip
=> 0x804833e <main+8>: mov $0x1,%eax
0x8048343 <main+13>: mov $0x0,%ebx
0x8048348 <main+18>: int $0x80
0x804834a: add %al,(%eax)
(gdb) c
Setting and clearing the Sign Flag
Consider the following program:
.section .text
.globl main
main:
# set the sign flag
movl $-1, %eax
test %eax, %eax
# clear the sign flag
movl $42, %eax
test %eax, %eax
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
Now, let’s check it in GDB:
(gdb) disassemble main
Dump of assembler code for function main:
0x08048336 <+0>: mov $0xffffffff,%eax
0x0804833b <+5>: test %eax,%eax
0x0804833d <+7>: mov $0x2a,%eax
0x08048342 <+12>: test %eax,%eax
0x08048344 <+14>: mov $0x1,%eax
0x08048349 <+19>: mov $0x0,%ebx
0x0804834e <+24>: int $0x80
End of assembler dump.
(gdb) b * 0x08048336
Breakpoint 1 at 0x8048336
(gdb) display / $eflags
1: $eflags = <error: No registers.>
(gdb) run
Breakpoint 1, 0x08048336 in main ()
1: $eflags = [ PF ZF IF ]
(gdb) set $eflags=0x0
(gdb) ni
0x0804833b in main ()
1: $eflags = [ IF ]
(gdb) ni
0x0804833d in main ()
1: $eflags = [ PF SF IF ]
(gdb) ni
0x08048342 in main ()
1: $eflags = [ PF SF IF ]
(gdb) ni
0x08048344 in main ()
1: $eflags = [ IF ]
(gdb) c
Setting and clearing the zero flag
.section .text
.globl main
main:
# set zero flag (ZF)
movl $0, %eax
test %eax, %eax
# clear ZF
movl $42, %eax
test %eax, %eax
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
Controlling Execution Flow
Understanding EIP register
Consider the following program:
.section .text
.globl main
main:
# moving data
movl $4, %eax
movl $3, %ebx
movl $5, %ecx
movl $7, %edx
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
And let’s check it in GDB:
(gdb) disassemble main
Dump of assembler code for function main:
0x08048336 <+0>: mov $0x4,%eax
0x0804833b <+5>: mov $0x3,%ebx
0x08048340 <+10>: mov $0x5,%ecx
0x08048345 <+15>: mov $0x7,%edx
0x0804834a <+20>: mov $0x1,%eax
0x0804834f <+25>: mov $0x0,%ebx
0x08048354 <+30>: int $0x80
End of assembler dump.
(gdb) b * 0x08048336
Breakpoint 1 at 0x8048336
(gdb) run
Breakpoint 1, 0x08048336 in main ()
(gdb) info registers
edi 0xf7ffcc60 -134230944 # pointer to next instruction to be executed
(gdb) disassemble main
Dump of assembler code for function main:
=> 0x08048336 <+0>: mov $0x4,%eax # eip position
0x0804833b <+5>: mov $0x3,%ebx
0x08048340 <+10>: mov $0x5,%ecx
0x08048345 <+15>: mov $0x7,%edx
0x0804834a <+20>: mov $0x1,%eax
0x0804834f <+25>: mov $0x0,%ebx
0x08048354 <+30>: int $0x80
End of assembler dump.
(gdb)
Jump Instruction
Consider the following program:
.section .text
.globl main
main:
movl $1, %eax
movl $2, %ebx
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
Now, let’s implement jumps.
In assembly we have an instruction jmp which will jump to any memory location,
it takes only one argument which is the address that we want to jump to (or more
practically a label):
.section .text
.globl main
main:
movl $1, %eax
movl $2, %ebx
jmp jump_here
# exit
movl $1, %eax
movl $0, %ebx
int $0x80o
jump_here:
movl $100, %eax
movl $200, %ebx
As we are jumping to jump_here before the system exit call, it won’t be
executed.
Let’s check it in GDB:
(gdb) disassemble main
Dump of assembler code for function main:
0x08048336 <+0>: mov $0x1,%eax
0x0804833b <+5>: mov $0x2,%ebx
0x08048340 <+10>: jmp 0x804834e <jump_here>
0x08048342 <+12>: mov $0x1,%eax
0x08048347 <+17>: mov $0x0,%ebx
0x0804834c <+22>: int $0x80
End of assembler dump.
(gdb) disassemble jump_here
Dump of assembler code for function jump_here:
0x0804834e <+0>: mov $0x64,%eax
0x08048353 <+5>: mov $0xc8,%ebx
End of assembler dump.
(gdb) b * 0x08048340
Breakpoint 1 at 0x8048340
(gdb) run
Breakpoint 1, 0x08048340 in main ()
(gdb) disassemble main
Dump of assembler code for function main:
0x08048336 <+0>: mov $0x1,%eax
0x0804833b <+5>: mov $0x2,%ebx
=> 0x08048340 <+10>: jmp 0x804834e <jump_here>
0x08048342 <+12>: mov $0x1,%eax
0x08048347 <+17>: mov $0x0,%ebx
0x0804834c <+22>: int $0x80
End of assembler dump.
(gdb) ni
0x0804834e in jump_here ()
(gdb) disassemble main
Dump of assembler code for function main:
0x08048336 <+0>: mov $0x1,%eax
0x0804833b <+5>: mov $0x2,%ebx
0x08048340 <+10>: jmp 0x804834e <jump_here>
0x08048342 <+12>: mov $0x1,%eax
0x08048347 <+17>: mov $0x0,%ebx
0x0804834c <+22>: int $0x80
End of assembler dump.
(gdb) disassemble jump_here
Dump of assembler code for function jump_here:
=> 0x0804834e <+0>: mov $0x64,%eax
0x08048353 <+5>: mov $0xc8,%ebx
End of assembler dump.
(gdb)
The call instruction
Consider the following program:
.section .text
.globl main
main:
movl $3, %eax
movl $6, %ebx
call location 1
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
location1:
movl $99, %eax
movl $77, %ebx
ret
The difference between jmp and call, is that it will return back to the
execution point once the instructions in the label we have called reaches
ret. Therefore, once the line movl $77, %eax is executed the %eip will point
back to the main label and the exit system call will be executed.
Conditional Jump
In assembly we have an instruction cmp which works like this:
cmp source, destination
That instruction allows us to compare two values, and get a result. The way that this instruction works is by subtracting the source to the destination, as if we wrote this:
cmp %eax, %ebx # equivalent to %ebx - %eax
When the subtraction is executed, in case they are equal the result would be zero (setting the ZF). In case that the result of the subtraction is not zero, then the ZF won’t be set.
Now, let’s look at it in a real program:
.section .data
msg_success:
.ascii "eq"
msg_failure:
.ascii "ne"
.section .text
.globl main
main:
movl $4, %eax
movl $8, %ebx
cmp %eax, %ebx
je jump_equals
jne jump_not_equals
jump_equals:
movl $4, %eax
movl $1, %ebx
movl $msg_success, %ecx
movl $2, %edx
int $0x80
jmp exit
jump_not_equals:
movl $4, %eax
movl $1, %ebx
movl $msg_failure, %ecx
movl $2, %edx
int $0x80
exit:
movl $1, %eax
movl $0, %ebx
int $0x80
That program uses the je (jump if equals) and jne (jump if not equals)
instructions which will be used to jump to either jump_equals in case the values
of %eax and %ebx are equal, and jump_not_equals in case they are not.
Using Zero Flag as a Conditional Jump
Consider the following program:
.section .data
msg:
.ascii "zero"
.section .text
.globl main
main:
movl $4, %eax
subl $4, %eax # %eax is now 0
jz jump_here # jump if ZF is set
exit:
movl $1, %eax
movl $0, %ebx
int $0x80
jump_here:
movl $4, %eax
movl $1, %ebx
movl $msg, %ecx
movl $4, %edx
int $0x80
jmp exit
jz will jump to the specified label if the ZF is set.
Using Overflow Flag as a Conditional Jump
Consider the following program:
.section .data
msg:
.ascii "of"
.section .text
.globl main
main:
movl $0x7fffffff, %eax
addl $1, %eax # this should set the OF
jo jump_here
exit:
movl $1, %eax
movl $0, %ebx
int $0x80
jump_here:
movl $4, %eax
movl $1, %ebx
movl $msg, %ecx
movl $2, %edx
int $0x80
jmp exit
jo will jump to the specified label if the OF flag is set.
Parity Flag as a Conditional Jump
Consider the following program:
.section .data
msg:
.ascii "pf"
.section .text
.globl main
main:
movl $4, %eax
subl $1, %eax # this should set the PF
jp jump_here
exit:
movl $1, %eax
movl $0, %ebx
int $0x80
jump_here:
movl $4, %eax
movl $1, %ebx
movl $msg, %ecx
movl $2, %edx
int $0x80
jmp exit
jp will jump to the specified label if the PF is set.
Using Numbers
Types of Numbers Used in Assembly
In assembly we have different number types:
- Integers
- Unsigned
- Signed
- SIMD Integers
- MMX Integers (Multimedia Extension)
- SSE Integers (Streaming SIMD Extensions)
- Binary Coded Decimal
- Floating Point Numbers
Signed and Unsigned Integers
Signed numbers, are those that have a sign, basically, those that are negative. Some examples could be:
- -2
- -10
- -100
And we can think of unsigned numbers, as positive integers:
- 2
- 10
- 100
The way that signed numbers are stored in memory is like this. Suppose this is a 32-bit register:
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
If we take the lowest 8-bits:
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--/--+--+--+--+--+--+--+--/
/ /
/ /
/ /
/ /
/ /
/ /
/ /
+--+--+--+--+--+--+--+--+
| | | | | | | | |
+--+--+--+--+--+--+--+--+
-128 -3 -2 -1 0 1 2 3 127
We can store an integer ranging from -128 to 127 (having 128 negative values, and 128 positive values - including zero). If we want to store -129, for example, it won’t be possible as our 8-bit register won’t be able to hold it.
But now, let’s suppose we want to store -27:
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--/
/ / /
/ / /
/ / /
/ / /
/ / /
/ / /
/ / /
+--+--+--+--+--+--+--+--+
|--| 7 bit |
++-+--+--+--+--+--+--+--+
| |
| |
Used for the sign Other 7 bits store
the value
In the 8-bit that we have available to store the number, the highest bit will be used to store its sign (1 if it’s negative, 0 if it’s positive), and the other 7 bits will be used to store the actual value.
Unsigned integers
Consider a 32-bit register:
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
How an unsigned number is stored into memory is like this:
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--/--+--+--+--+--+--+--+--/
/ /
/ /
/ /
/ /
/ /
/ /
/ /
+--+--+--+--+--+--+--+--+
| |
+--+--+--+--+--+--+--+--+
0 1 2 3 4 5 6 255
When storing an unsigned integer we have an extra bit to store data, resulting in a number ranging from 0, to 255 (having 256 numbers to store).
Using unsigned integers
Consider the following program:
.section .data
number: .byte 256 # overflow 0 ... 255
.section .text
.globl main
main:
movb number, %al # moving 256 into al register
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
We might get warnings like this:
test.s: Assembler messages:
test.s:2: Warning: value 0x100 truncated to 0x0
It means that our 256 was replaced with 0. So the value of number will be 0.
How a CPU Stores the Numbers in Memory
The way that CPU stores numbers in memory is the following:
Decimal
+----------+ +------------+ +------------+
| 12345678 +--------------+ 0xbc614e +---------------------+ 0x4e61bc |
+----------+ +------------+ +------------+
Convert to Store in reverse
Hex order
How to Use Signed Numbers
Consider the following program:
.section .data
number1: .int -27
number2: .int -3
.section .text
.globl main
main:
movl number1, %eax
movl number2, %ebx
addl %eax, %ebx # result stored in %ebx
# exit
movl $1, %eax
movl $0, %ebx
int $0x80
And let’s check it in GDB:
(gdb) disassemble main
Dump of assembler code for function main:
0x08048336 <+0>: mov 0x804b008,%eax
0x0804833b <+5>: mov 0x804b00c,%ebx
0x08048341 <+11>: add %eax,%ebx
0x08048343 <+13>: mov $0x1,%eax
0x08048348 <+18>: mov $0x0,%ebx
0x0804834d <+23>: int $0x80
End of assembler dump.
(gdb) b * 0x08048341
Breakpoint 1 at 0x8048341
(gdb) run
Breakpoint 1, 0x08048341 in main ()
(gdb) info reg
eax 0xffffffe5 -27
ebx 0xfffffffd -3
(gdb) ni
0x08048343 in main ()
(gdb) disassemble
Dump of assembler code for function main:
0x08048336 <+0>: mov 0x804b008,%eax
0x0804833b <+5>: mov 0x804b00c,%ebx
0x08048341 <+11>: add %eax,%ebx
=> 0x08048343 <+13>: mov $0x1,%eax
0x08048348 <+18>: mov $0x0,%ebx
0x0804834d <+23>: int $0x80
End of assembler dump.
(gdb) p/d $ebx
$1 = -30
(gdb)
SIMD registers
SIMD registers have two types:
- MMX (Multimedia Extension): 64-bit in size.
- SSE (Streaming SIMD Extensions): 128-bit in size.
These registers are used to store different size of integers.
-
MMX Registers in assembly
MMX registers range from
mm0tomm7. In these 64-bit registers, we can store 8 different 8-byte integers. We can also store 4 16-bit integers. We can store 2 32-bit integers.We can move data into these registers like this:
movq source, destinationNow, let’s see it in an actual program:
.section .data value1: .int 1, 2 .section .text .globl main main: movq value1, %mm0 # exit movl $1, %eax movl $0, %ebx int $0x80Let’s check it in GDB:
(gdb) disassemble main Dump of assembler code for function main: 0x08048336 <+0>: movq 0x804b008,%mm0 0x0804833d <+7>: mov $0x1,%eax 0x08048342 <+12>: mov $0x0,%ebx 0x08048347 <+17>: int $0x80 End of assembler dump. (gdb) b * 0x08048336 Breakpoint 1 at 0x8048336 (gdb) run Breakpoint 1, 0x08048336 in main () (gdb) disassemble Dump of assembler code for function main: => 0x08048336 <+0>: movq 0x804b008,%mm0 0x0804833d <+7>: mov $0x1,%eax 0x08048342 <+12>: mov $0x0,%ebx 0x08048347 <+17>: int $0x80 End of assembler dump. (gdb) p/x $mm0 $1 = {uint64 = 0x0, v2_int32 = {0x0, 0x0}, v4_int16 = {0x0, 0x0, 0x0, 0x0}, v8_int8 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}} (gdb) ni 0x0804833d in main () (gdb) p/x $mm0 $2 = {uint64 = 0x200000001, v2_int32 = {0x1, 0x2}, v4_int16 = {0x1, 0x0, 0x2, 0x0}, v8_int8 = {0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0}} (gdb)
-
Understanding SSE Registers
SSE Registers (Streaming SIMD [Simple Instruction Multiple Data] Extensions) are 128-bit sized registers. Their names are:
xmm0xmm1xmm2xmm3xmm4xmm5xmm6xmm7
We can store 16 single byte integers, we can store 8 16-bit integers, we can store 4 32-bit integers, we can store 2 64-bit integers.
We can move data into these registers like this:
movdqa source, destinationmovdqastands for mov double quadword aligned. And we can also usemovdquwhich stands for move double quadword unaligned.
-
Using SSE Registers
Consider the following program:
.section .data value1: .int 0, 1, 2, 3 value2: .byte 0, 1, 2, 3, 4, 5, 6, 7 .section .text .globl main main: movdqu value1, %xmm0 movdqu value2, %xmm1 # exit movl $1, %eax movl $0, %ebx int $0x80Let’s check it in GDB:
(gdb) disassemble main Dump of assembler code for function main: 0x08048336 <+0>: movdqu 0x804b008,%xmm0 0x0804833e <+8>: movdqu 0x804b018,%xmm1 0x08048346 <+16>: mov $0x1,%eax 0x0804834b <+21>: mov $0x0,%ebx 0x08048350 <+26>: int $0x80 End of assembler dump. (gdb) b main Breakpoint 1 at 0x8048336 (gdb) run Breakpoint 1, 0x08048336 in main () (gdb) disassemble Dump of assembler code for function main: => 0x08048336 <+0>: movdqu 0x804b008,%xmm0 0x0804833e <+8>: movdqu 0x804b018,%xmm1 0x08048346 <+16>: mov $0x1,%eax 0x0804834b <+21>: mov $0x0,%ebx 0x08048350 <+26>: int $0x80 End of assembler dump. (gdb) ni 0x0804833e in main () (gdb) p/x $xmm0 $1 = {v8_bfloat16 = {0x0, 0x0, 0x1, 0x0, 0x2, 0x0, 0x3, 0x0}, v8_half = {0x0, 0x0, 0x1, 0x0, 0x2, 0x0, 0x3, 0x0}, v4_float = {0x0, 0x1, 0x2, 0x3}, v2_double = {0x100000000, 0x300000002}, v16_int8 = {0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0}, v8_int16 = {0x0, 0x0, 0x1, 0x0, 0x2, 0x0, 0x3, 0x0}, v4_int32 = {0x0, 0x1, 0x2, 0x3}, v2_int64 = {0x100000000, 0x300000002}, uint128 = 0x3000000020000000100000000} (gdb) ni 0x08048346 in main () (gdb) p/x $xmm1 $2 = {v8_bfloat16 = {0x100, 0x302, 0x504, 0x706, 0x0, 0x0, 0x0, 0x0}, v8_half = {0x100, 0x302, 0x504, 0x706, 0x0, 0x0, 0x0, 0x0}, v4_float = {0x3020100, 0x7060504, 0x0, 0x0}, v2_double = {0x706050403020100, 0x0}, v16_int8 = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v8_int16 = {0x100, 0x302, 0x504, 0x706, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x3020100, 0x7060504, 0x0, 0x0}, v2_int64 = {0x706050403020100, 0x0}, uint128 = 0x706050403020100}