Lab 3 – Compiled C

In this lab we are looking at how different compiler options effects the output of our source code. All these tests were done in a aarch64 environment.

We will be using this basic program for testing:


int main() {
printf("Hello World!\n");

Basic Case

Our basic compile case is
gcc -g -O0 -fno-builtin -o initial source.c

Looking at the result with objdump we see that our code is in the section labeled main, starting at the address 400594. And our string is at address 400480.

The resulting file is 72K bytes.


gcc -g -O0 -fno-builtin -static -o static source.c

In this case we add -static to the gcc command.

This file is 617k bytes.

This is because the compiler is forced to use a static library. This means that instead of calling a dynamic library like the pervious version, the compiled version of stdio is added into this file. You can see this with the call to printf not being through the plt but to a address in this file.

This is far less efficient in terms of the size of the file. However has other benefits such as the speed of the function call.


Now we are compiling without fno-builtin

gcc -g -O0 -o nobuiltin source.c

This file is 72K bytes.

Without the fno-builtin option the compiler will optimize function calls. This means it will use the best function for what it is doing. In this case it has switched from calling printf to calling puts. As the value is a fixed string with no other parameters it is more efficient to use puts, so the compiler changes the call.

no -g

Now we are compiling without the -g option.
gcc -O0 -fno-builtin -o nog source.c

This file is 69K bytes.

The -g option includes debug information in the resulting code. The -g option increases file size but improves error messages and makes the resulting file significantly more readable.

More args

Now to modify our source code to have more args in the printf call.


int main(){
printf("Hello World!\n",1,2,3,4,5,6,7,8,9,10);

And compile with our basic case
gcc -g -O0 -fno-builtin -o sargs source-args.c

What we see now is that it operates on the arguments in reverse order.
Arguments 10 – 8 get shifted and put into w0, requiring 2 operations for each argument.
7 – 1 get put in registers w7 – w1 respectively, requiring 1 operation each.

output function

This time we wrap the printf call in another function.


int main(){

void output(){
printf("Hello World!\n");

Again compiling with out default case.

gcc -g -O0 -fno-builtin -o output output.c

In the resulting file we can see that the only real difference is the overhead of the function call. Resulting in 6 extra operations over the initial version. There is a branch and link to the output label, and then the stack pointer moving and returning.

-O0 vs -O3

Now to compile our initial source code. With -O3 instead of -O0 compiler arguments.
gcc -g -O3 -fno-builtin -o O3 source.c

The -O3 option will compile with optimization. All this seems to do to the main function is move the first mov to below adrp and add. Im not sure what this optimization is for, however compiler optimization is usually a good idea.