Lab 9 - Functions
Task: Displaying a String
Navigate to chapters/stack/functions/drills/tasks/string-print/support/
.
To display a string, we can use the internal macro PRINTF32
.
Alternatively, we can use a function such as puts()
.
In the file print_string.asm
, displaying a string using the PRINTF32
macro is implemented.
Following the example of the hello_world.asm
file, implement string display using puts
as well.
If you're having difficulties solving this exercise, take a peek at hello_world.asm.
If you're having trouble solving this exercise, go through this reading material.
Task: Displaying the Length of a String
Navigate to chapters/stack/functions/drills/tasks/string-print-len/support/
.
The program print_string_len.asm
displays the length of a string using the PRINTF32
macro.
The calculation of the length of the mystring
string occurs within the program (it is already implemented).
Implement the program to display the length of the string using the printf
function.
At the end, you will have the length of the string displayed twice: initially with the PRINTF32
macro and then with the external function call printf
.
NOTE: Consider that the
printf
call is of the formprintf("String length is %u\n", len);
. You need to construct the stack for this call.The steps to follow are:
- Mark the symbol
printf
as external.- Define the format string
"String length is %u", 10, 0
.- Make the function call to
printf
, i.e.:
- Put the two arguments on the stack: the format string and the length.
- Call
printf
usingcall
.- Restore the stack.
The length of the string is found in the
ecx
register.
If you're having trouble solving this exercise, go through this reading material.
Task: Displaying the Reversed String
Navigate to chapters/stack/functions/drills/tasks/print-rev-string/support/
.
In the file print_rev_string.asm
, add the reverse_string()
function so that you have a listing similar to the one below:
[...]
section .text
global main
reverse_string:
push ebp
mov ebp, esp
mov eax, [ebp + 8]
mov ecx, [ebp + 12]
add eax, ecx
dec eax
mov edx, [ebp + 16]
copy_one_byte:
mov bl, [eax]
mov [edx], bl
dec eax
inc edx
loopnz copy_one_byte
inc edx
mov byte [edx], 0
leave
ret
main:
push ebp
mov ebp, esp
[...]
IMPORTANT: When copying the
reverse_string()
function into your program, remember that the function starts at thereverse_string()
label and ends at themain
label. Thecopy_one_byte
label is part of thereverse_string()
function.
The reverse_string()
function reverses a string and has the following signature: void reverse_string(const char *src, size_t len, char *dst);
.
This means that the first len
characters of the src
string are reversed into the dst
string.
Reverse the mystring
string into a new string and display that new string.
NOTE: To define a new string, we recommend using the following construction in the data section:
store_string times 64 db 0
This creates a string of 64 zero bytes, enough to store the reverse of the string. The equivalent C function call is
reverse_string(mystring, ecx, store_string);
. We assume that the length of the string is calculated and stored in theecx
register.You cannot directly use the value of
ecx
in its current form. After theprintf()
function call for displaying the length, the value ofecx
is not preserved. To retain it, you have two options:
- Store the value of the
ecx
register on the stack beforehand (usingpush ecx
before theprintf
call) and then restore it after theprintf
call (usingpop ecx
).- Store the value of the
ecx
register in a global variable, which you define in the.data
section.You cannot use another register because there is a high chance that even that register will be modified by the
printf
call to display the length of the string.
If you're having trouble solving this exercise, go through this reading material.
Task: Implementing the toupper()
Function
Navigate to chapters/stack/functions/drills/tasks/to-upper/support/
.
We aim to implement the toupper()
function, which converts lowercase letters to uppercase.
To do this, start with the to_upper.asm
file from the lab exercises archive and complete the body of the toupper()
function.
The string used is mystring
, and we assume it is a valid string.
This string is passed as an argument to the toupper()
function when called.
Perform the transformation in place; there is no need for another string.
NOTE_ To convert a lowercase letter to uppercase, you need to subtract
0x20
from its value. This is the difference between lowercase and uppercase letters; for example,a
is0x61
, andA
is0x41
. You can see this in the ASCII manual page.To read or write byte by byte, use the
byte [reg]
construction as seen in the implementation of determining the length of a string in theprint_string_length.asm
file, where[reg]
is the pointer register storing the address of the string at that point.Stop when you reach the value
0
(NULL
byte). For checking, you can usetest
as seen in the implementation of determining the length of a string in theprint-string-length.asm
file.
Bonus: toupper()
Only for Lowercase Letters
Implement the toupper()
function so that the transformation occurs only for lowercase characters, not uppercase letters or other types of characters.
If you're having trouble solving this exercise, go through this reading material.
Task: Bonus: Rot13
Navigate to chapters/stack/functions/drills/tasks/rot13/support/
.
Create and use a function that performs rot13 translation of a string.
Bonus: Rot13++
Implement rot13
on an array of strings: the strings are contiguous in memory separated by the string terminator (NULL
-byte, 0
).
For example, lorem\0ipsum\0dolor\0
is an array of three strings.
Apply rot13
to alphabetical characters and replace the string terminator with a space (' '
, blank, character 32
, or 0x20
).
Thus, the initial string lorem\0ipsum\0dolor\0
will translate to yberz vcfhz qbybe
.
NOTE: To define the array of strings containing the string terminator, use a construction like:
mystring db "lorem", 0, "ipsum", 0, "dolor", 0
NOTE: You will need to know when to stop traversing the array of strings. The simplest way is to define a length variable in the
.data
section, like so:len dd 10
where you either store the total length of the string (from the beginning to the last
NULL
byte) or the number of strings in the array.
If you're having trouble solving this exercise, go through this reading material.
Functions
In this lab, we'll discuss how function calls are performed.
We'll see how we can use the call
and ret
instructions to make function calls and how we use the stack to pass function parameters.
Passing Parameters
When it comes to calling a function with parameters, there are two major options for placing them:
Register Passing - this method intuitively involves passing parameters through registers.
Advantages:
- It is very easy to use when the number of parameters is small.
- It is very fast since parameters are immediately accessible from registers.
Disadvantages:
- Because there is a limited number of registers, the number of parameters for a function becomes limited.
- It's very likely that some registers are used inside the called function, and it becomes necessary to temporarily save registers on the stack before the function call. Thus, the second advantage listed disappears because accessing the stack involves working with memory, meaning increased latency.
Stack Passing - this method involves pushing all parameters onto the stack.
Advantages:
- A large number of parameters can be passed.
Disadvantages:
- It's slow because it involves memory access.
- More complicated in terms of parameter access.
NOTE: For 32-bit architectures, the stack passing method is used, while for 64-bit architectures, the register passing method is used for the first 6 arguments. Starting from the 7th, the stack has to be used. We will use the convention for 32-bit architecture.
Function Call
When we call a function, the steps are as follows:
- We put the arguments on the stack, pushing them in the reverse order in which they are sent as function arguments.
- We call
call
. - We restore the stack at the end of the call.
Stack Operation
As we know, stack operations fall into two types:
push val
where the valueval
is placed on the stackpop reg/mem
where what is on the top of the stack is placed into a register or memory area
When we push
, we say that the stack grows (elements are added).
For reasons that will be better explained later, the stack pointer (indicated by the esp
register in 32-bit mode) decreases in value when the stack grows (on push
).
However, this contradiction in naming comes from the fact that the stack is typically represented vertically, with smaller values at the top and larger values at the bottom.
Similarly, when we pop
, we say that the stack shrinks (elements are removed).
Now the stack pointer (indicated by the esp
register in 32-bit mode) increases in value.
A summary of this is explained very well here.
For example, if we have the function foo
with the following signature (in C language):
int foo(int a, int b, int c);
The call to this function will look like this:
mov ecx, [c] ; take the value of parameter c from a memory location
mov ebx, [b]
mov eax, [a]
push ecx ; put parameters in reverse order, starting with c
push ebx ; then b
push eax ; then a
call foo ; call the function
add esp, 12 ; restore the stack
Caller and Callee
When we call a function, we say that the calling function (the context that calls) is the caller, while the called function is the callee. In the previous paragraph, we discussed how things look at the caller level (how we build the stack).
Now let's see what happens at the callee level.
Until the call
instruction, the stack contains the function's parameters.
The call
can be roughly equated to the following sequence:
push eip
jmp function_name
That is, even the call
uses the stack and saves the address of the next instruction, the one after the call
, also known as the return address.
This is necessary for the callee to know where to return to in the caller.
In the callee, at its beginning (called preamble), the frame pointer is saved (in the i386 architecture, this is the ebp
register), with the frame pointer then referring to the current function stack frame.
This is crucial for accessing parameters and local variables via an offset from the frame pointer.
Although not mandatory, saving the frame pointer helps in debugging and is used in most cases. For these reasons, any function call will generally have a preamble:
push ebp
mov ebp, esp
These modifications take place in the callee. Therefore, it is the responsibility of the callee to restore the stack to its old value. Hence, it is customary to have an epilogue that restores the stack to its initial state; this epilogue is:
leave
After this instruction, the stack is as it was at the beginning of the function (immediately after the call). It is equivalent to the following code, which undoes the functions's preamble:
mov esp, ebp
pop ebp
To conclude the function, it is necessary for the execution to return and continue from the instruction following the call
that started the function.
This involves influencing the eip
register and putting back the value that was saved on the stack initially by the call
instruction.
This is achieved using the instruction:
ret
which is roughly equivalent to the instruction:
pop eip
For example, the definition and body of the function foo, which calculates the sum of 3 numbers, would look like this:
foo:
push ebp
mov ebp, esp
mov eax, [ebp + 8]
mov ebx, [ebp + 12]
mov ecx, [ebp + 16]
add eax, ebx
add eax, ecx
leave
ret
Remarks
A function is defined by a label.
After the function's preamble, the stack looks as follows:
Note that during the execution of the function, what does not change is the position of the frame pointer. This is the reason for its name: it points to the current function's frame. Therefore, it is common to access a function's parameters through the frame pointer. Assuming a 32-bit system and processor word-sized parameters (32 bits, 4 bytes), we will have:
the first argument is found at address
ebp+8
the second argument is found at address
ebp+12
the third argument is found at address
ebp+16
etc.
This is why, to get the parameters of the foo function in the eax, ebx, ecx registers, we use the constructions:
mov eax, dword [ebp+8] ; first argument in eax
mov ebx, dword [ebp+12] ; second argument in ebx
mov ecx, dword [ebp+16] ; third argument in ecx
The return value of a function is placed in registers (generally in eax).
If the return value is 8 bits, the function's result is placed in
al
.If the return value is 16 bits, the function's result is placed in
ax
.If the return value is 32 bits, the function's result is placed in
eax
.If the return value is 64 bits, the result is placed in the
edx
andeax
registers. The most significant 32 bits are placed inedx
, and the rest in theeax
register.Additionally, in some cases, a memory address can be returned to the stack/heap (e.g.
malloc()
), or other memory areas, which refer to the desired object after the function call.
A function uses the same hardware registers; therefore, when exiting the function, the values of the registers are no longer the same. To avoid this situation, some/all registers can be saved on the stack. You can push all registers to the stack using the
pusha
instruction - "push all". And you can pop them all in the same order usingpopa
. The disadvantage of doing so is that writing all registers to the stack is going to be slower than only explicitly saving the registers used by the function. For this reason, thecdecl
calling convention specifies that functions are allowed to change the values of theeax
,ecx
andedx
registers.
NOTE: Since assembly languages offer more opportunities, there is a need for calling conventions in x86. The difference between them may consist of the parameter order, how the parameters are passed to the function, which registers need to be preserved by the callee or whether the caller or callee handles stack preparation. More details can be found here or here if Wikipedia is too mainstream for you. For us, the registers
eax
,ecx
,edx
are considered clobbered (or volatile), and the callee can do whatever it wants to them. On the other hand, the callee has to ensure thatebx
exits the function with the same value it has entered with.
Guide: Hello, World
Navigate to chapters/stack/functions/guides/hello-world/support/
.
Open the hello_world.asm
file, assemble it, and run it.
Notice the display of the message Hello, world!
Note that:
- The
hello_world.asm
program uses theputs()
function call (an external function of the current module) to perform the display. For this, it puts the argument on the stack and calls the function. - The
msg
variable in thehello_world.asm
program contains the byte10
. This symbolizes the line feed character (\n
), used to add a new line on Linux.
Ending with \n
is generally useful for displaying strings.
The puts()
function automatically adds a new line after the displayed string, but this must be explicitly added when using the printf()
function.
Guide: Disassembling a C program
Navigate to chapters/stack/functions/guides/disassembling-c/support/
.
As mentioned, ultimately everything ends up in assembly language (to be 100% accurate, everything ends up as machine code, which has a fairly good correspondence with assembly code). Often, we find ourselves with access only to the object code of some programs and we want to inspect how it looks.
To observe this, let's compile a C program to its object code and then disassemble it.
We'll use the test.c
program from the lab archive.
NOTE: To compile a C/C++ source file in the command-line, follow these steps:
Open a terminal. (shortcut
Ctrl+Alt+T
)Navigate to the directory containing your source code.
Use the command:
gcc -m32 -o <exec> <sourcefile>
where <sourcefile>
is the name of the source file (test.c
) and <exec>
is the name of the result executable.
If you only want to compile (without linking it), use:
gcc -m32 -c -o <objfile> <sourcefile>
where <sourcefile>
is the name of the source file and <objfile>
is the name of the desired output object file.
Since we want to transform test.c
into an object file, we'll run:
gcc -m32 -c -o test.o test.c
After running the above command, we should see a file named test.o
.
Furthermore, we can use gcc
to transform the C
code in Assembly
code:
gcc -m32 -masm=intel -S -o test.asm test.c
After running the above command we'll have a file called test.asm
, which we can inspect using any text editor/reader, such as cat:
cat test.asm
In order to disassembly the code of an object file we'll use objdump
as follows:
objdump -M intel -d <path-to-obj-file>
where <path-to-obj-file>
is the path to the object file test.o
.
Afterwards, you'll see an output similar to the following:
$ objdump -M intel -d test.o
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <second_func>:
0: 55 push ebp
1: 89 e5 mov ebp,esp
3: e8 fc ff ff ff call 4 <second_func+0x4>
8: 05 01 00 00 00 add eax,0x1
d: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
10: 8b 10 mov edx,DWORD PTR [eax]
12: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
15: 01 c2 add edx,eax
17: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
1a: 89 10 mov DWORD PTR [eax],edx
1c: 90 nop
1d: 5d pop ebp
1e: c3 ret
0000001f <first_func>:
1f: 55 push ebp
20: 89 e5 mov ebp,esp
22: 53 push ebx
23: 83 ec 14 sub esp,0x14
26: e8 fc ff ff ff call 27 <first_func+0x8>
2b: 05 01 00 00 00 add eax,0x1
30: c7 45 f4 03 00 00 00 mov DWORD PTR [ebp-0xc],0x3
37: 83 ec 0c sub esp,0xc
3a: 8d 90 00 00 00 00 lea edx,[eax+0x0]
40: 52 push edx
41: 89 c3 mov ebx,eax
43: e8 fc ff ff ff call 44 <first_func+0x25>
48: 83 c4 10 add esp,0x10
4b: 83 ec 08 sub esp,0x8
4e: ff 75 f4 push DWORD PTR [ebp-0xc]
51: 8d 45 08 lea eax,[ebp+0x8]
54: 50 push eax
55: e8 a6 ff ff ff call 0 <second_func>
5a: 83 c4 10 add esp,0x10
5d: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
60: 8b 5d fc mov ebx,DWORD PTR [ebp-0x4]
63: c9 leave
64: c3 ret
00000065 <main>:
65: 8d 4c 24 04 lea ecx,[esp+0x4]
69: 83 e4 f0 and esp,0xfffffff0
6c: ff 71 fc push DWORD PTR [ecx-0x4]
6f: 55 push ebp
70: 89 e5 mov ebp,esp
72: 53 push ebx
73: 51 push ecx
74: e8 fc ff ff ff call 75 <main+0x10>
79: 81 c3 02 00 00 00 add ebx,0x2
7f: 83 ec 0c sub esp,0xc
82: 6a 0f push 0xf
84: e8 96 ff ff ff call 1f <first_func>
89: 83 c4 10 add esp,0x10
8c: 83 ec 08 sub esp,0x8
8f: 50 push eax
90: 8d 83 0e 00 00 00 lea eax,[ebx+0xe]
96: 50 push eax
97: e8 fc ff ff ff call 98 <main+0x33>
9c: 83 c4 10 add esp,0x10
9f: b8 00 00 00 00 mov eax,0x0
a4: 8d 65 f8 lea esp,[ebp-0x8]
a7: 59 pop ecx
a8: 5b pop ebx
a9: 5d pop ebp
aa: 8d 61 fc lea esp,[ecx-0x4]
ad: c3 ret
There are many other utilities that allow disassembly of object modules, most of them with a graphical interface and offering debugging support.
objdump
is a simple utility that can be quickly used from the command-line.
It's interesting to observe, both in the test.asm
file and in its disassembly, the way a function call is made, which we'll discuss further.