Skip to main content

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 form printf("String length is %u\n", len);. You need to construct the stack for this call.

The steps to follow are:

  1. Mark the symbol printf as external.
  2. Define the format string "String length is %u", 10, 0.
  3. Make the function call to printf, i.e.:
    1. Put the two arguments on the stack: the format string and the length.
    2. Call printf using call.
    3. 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 the reverse_string() label and ends at the main label. The copy_one_byte label is part of the reverse_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 the ecx register.

You cannot directly use the value of ecx in its current form. After the printf() function call for displaying the length, the value of ecx is not preserved. To retain it, you have two options:

  1. Store the value of the ecx register on the stack beforehand (using push ecx before the printf call) and then restore it after the printf call (using pop ecx).
  2. 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 is 0x61, and A is 0x41. 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 the print_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 use test as seen in the implementation of determining the length of a string in the print-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:

  1. 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.
  2. 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 value val is placed on the stack
  • pop 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

  1. A function is defined by a label.

  2. After the function's preamble, the stack looks as follows:

    stack.svg

  3. 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
  4. 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 and eax registers. The most significant 32 bits are placed in edx, and the rest in the eax 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.

  5. 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 using popa. 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, the cdecl calling convention specifies that functions are allowed to change the values of the eax, ecx and edx 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 that ebx 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 the puts() 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 the hello_world.asm program contains the byte 10. 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:

  1. Open a terminal. (shortcut Ctrl+Alt+T)

  2. Navigate to the directory containing your source code.

  3. 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.