Exploring Assembly Code

As the project I will be tackling involves porting x86_64 programs to AArch64, I must familiarize myself with assembly code. To get the basics down, I did something noone has ever done before: created a Hello World program in C.

#include <stdio.h>

int main() {

printf(“Hello World\n”);

return 0;
}

I then compiled it using the gcc compiler in linux, along with the options:

-fno-builtin which does not optimize functions in the C code

-g which allows for debugging options

-O0 which turns off compiler optimization

Using the objdump program in linux, I then played around with the code a bit, as well as various arguments when compiling, to see exactly what happens when code is compiled. The analysis was illuminating.

First, the plain code compiled is called an ELF, which stands for Executable and Linked Format. It showed a whole bunch of assembly functions run under a bunch of sections. The sections I focused on were .text, which is the actual code run, .data and .rodata (read-only data), which is data referenced in the disassembly code.

The following is taken from the code analysed:

img1

As you can see, this is the main method in assembly. This code is underneath the .text section. What happens is it creates a stack in memory and then grabs the address 0x4005e0 and points to it. What is at that address? Well:

img2

The text “Hello World”! That data which now has a fixed pointer is now ready, at instruction 40053e, to call the printf function from the .plt section. When it calls the function, it shows the line to find it which is 400410.

img3

img4

After running the printf function, it pops off  the one item on the stack and closes the program. Fairly straightforward.

Experiment 1: Add -static argument to gcc compilation

When running this, the first thing you notice that is glaringly obvious is when you look at the directory listing

img5

The size of the normal file is 9590 bits (about 9KB) and the static one is 842326 bits (about 842KB)! The reason for this is that the static option disables dynamic linking, meaning that instead of being able to call out to shared libraries (like stdio.h), the entire library is imported into the compiled library!

When looking at the dump of the section headers, you can see that all the dynamic sections are removed, like .gnu_hash, and other sections are added in, such as freeres.

Also, rather than printf@plt, a stub that makes a call to the Global Library in the regular ELF, being called, it calls its parent function IO_printf which eventually makes its way around the library to run the printf function.

Experiment 2: Remove the compiler option -fno-builtin

This argument disables function optimization. When it is left out of the compilation, the main method does this:

img6

In the normal code, printf was called at that point (which was in my original code). So what gives? Well, printf() and puts() do the same thing….. if only one argument is passed to the function. printf() allows multiple arguments but eventually it will call puts(). What the function optimizer does is see that only one argument, “Hello World”, is passed to printf() meaning that it knows that it can skip the processing it normally does and just call puts() right off the bat.

Experiment 3: Remove compiler option -g

-g allows debugging. Debugging means extra information left in the ELF so that humans browsing it can have an easier time making sense of it, but it’s not actually needed to run. The compiled file is 8558 bits large vs 9590. The disassembly for this file looked near identical but when viewing the section headers, all the debug information is gone. In the following snapshot, everything below the red line does not exist when compiled with debugging enabled

img7

Experiment 4: Adding additional arguments to printf() in C code

Just what it says:

img8Which outputs this:

img9

When viewing the dump of the code, what I see is this:

img10

Where the first red circle is, it creates the stack and allocates a certain amount of registers to it; in this case, it’s 30 in hex, or 48 in decimal. It then proceeds to start adding the arguments from printf() to memory, starting from a in hex (or 10 in decimal) and working backwards. At one point, it gets to the end of the stack (where the second red circle is) and yet it hasn’t finished saving all the arguments, so it starts specifying registers to save them to, outside the stack. It then gets the first argument (ie, the part between the quotes) and passes that to printf.

Experiment 5: Use an external function

In this experiment, I modified the C code by creating a void output(char*) function, which does nothing but pass its argument to printf. Main method called output(). Here’s the disassembly code:

img11

The main method runs as it normally does, only it calls <output> instead of <printf@plt>, which is what I expected. What’s interesting is the circled part of the code, where the stack points to a section in memore and then some data is moved around, which I assume is the argument being taken from the function call and being localized. It then passes that off to printf, as normal.

What is notable about this is the extra processing the processor must do for functions, even though the program does the same thing.

Experiment 6: Replace -O0 with -O3

-O tells the compiler to optimize. -O0 means to do no optimization, -O1 tells it to do more, -O2 is even more, and -O3 is the most. This generally means that compilation takes quite a bit longer than it normally does. Another effect of it is that generally, it will output an ELF that is larger in size but performs faster.

On my test file, the size of the ELF went from 9590 to 11030 bits. What’s interesting is that the <main> function was bumped up to the top of the .text section, meaning that the computer does not jump around preparing memory for execution of the actual code. What I got out of this was that a well optimized program can seem to cut through the fat and get right to the meat of the code, allowing faster execution.

 

All in all, the code you write can’t just work, it must work well!

 

Leave a comment