Hello,
In the following post, I will be looking at seven different ways that you can compile a simple hello world program in C.
I will be using the following four flags for the GCC compiler.
-g # enable debugging information
-O0 # do not optimize
-fno-builtin # do not use builtin function optimizations
-static # includes the header files in the executable
After I compile using gcc, I will look at things like the filesize and the decompiled assembler to gather my results.
Okay, so test one is to compile the following program with -g
-O0
-fno-builtin
. This test will be the control for this experiment. We will be basing or conclusions for the other tests of this one.
$ gcc -g -O0 -fno-builtin -o test test.c
#include
int main(){
printf("Hello World!\n");
}
$ objdump -fsd --source test
The command “objdump” will allow me to view the compiled code. You will probably want to pipe this command into less or send to an output file.
The results of this command will be extensive, so you will want to use a search to find “<main>.”
// Total File Size 24656 bytes
#include <stdio.h>
int main(){
401126: 55 push %rbp
401127: 48 89 e5 mov %rsp,%rbp
printf("Hello World!\n");
40112a: bf 10 20 40 00 mov $0x402010,%edi
40112f: b8 00 00 00 00 mov $0x0,%eax
401134: e8 f7 fe ff ff callq 401030 <printf@plt>
401139: b8 00 00 00 00 mov $0x0,%eax
40113e: 5d pop %rbp
40113f: c3 retq
Alright, now we have our control results we can now start some tests.
Here is a link to an assembler quick start guide if you need.
TEST 1
Add the compiler option -static
.
$ gcc -static -g -O0 -fno-builtin -o test1 test.c
// Code
#include <stdio.h>
int main(){
printf("Hello World!\n");
}
$ objdump -fsd --source test1 >> test1.text
// Total File Size 1720896 bytes
#include
int main(){
401bb5: 55 push %rbp
401bb6: 48 89 e5 mov %rsp,%rbp
printf("Hello World!\n");
401bb9: bf 10 00 48 00 mov $0x480010,%edi
401bbe: b8 00 00 00 00 mov $0x0,%eax
401bc3: e8 f8 72 00 00 callq 408ec0 <_IO_printf>
401bc8: b8 00 00 00 00 mov $0x0,%eax
401bcd: 5d pop %rbp
401bce: c3 retq
401bcf: 90 nop
If we review the results the assembler, the “printf” gets called from inside the file versus linking to another file. And, if we look at the file size, it is now a lot larger. Due to the -static
, it has made it, so it included the header with the assembled code.
TEST 2
Remove the compiler option -fno-builtin
.
$ gcc -g -O0 -o test2 test.c
// Code
#include <stdio.h>
int main(){
printf("Hello World!\n");
}
$ objdump -fsd --source test2 >> test2.text
// Total File Size 24648 bytes
#include <stdio.h>
int main(){
401126: 55 push %rbp
401127: 48 89 e5 mov %rsp,%rbp
printf("Hello World!\n");
40112a: bf 10 20 40 00 mov $0x402010,%edi
40112f: e8 fc fe ff ff callq 401030 <puts@plt>
401134: b8 00 00 00 00 mov $0x0,%eax
401139: 5d pop %rbp
40113a: c3 retq
40113b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
If we review the results of the assembler, we can see that we are no longer using “printf” the compiler has replaced it with the “puts” function.
TEST 3
Remove the compiler option -g
.
$ gcc -O0 -fno-builtin -o ../excabutables/test3 test.c
// Code
#include <stdio.h>
int main(){
printf("Hello World!\n");
}
$ objdump -fsd --source test3 >> test3.text
// Total File Size 22272 bytes
401126: 55 push %rbp
401127: 48 89 e5 mov %rsp,%rbp
40112a: bf 10 20 40 00 mov $0x402010,%edi
40112f: b8 00 00 00 00 mov $0x0,%eax
401134: e8 f7 fe ff ff callq 401030 <printf@plt>
401139: b8 00 00 00 00 mov $0x0,%eax
40113e: 5d pop %rbp
40113f: c3 retq
If we review the results of the assembler, we can see that we are no longer able to see the debugger information like the header include, or code.
TEST 4
Add additional arguments to the printf()
function in your program.
$ gcc -g -O0 -fno-builtin -o test4 testMultiArgs.c
// Code
#include <stdio.h>
int main(){
printf("Hello World!!!, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d \n", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
$ objdump -fsd --source test4 >> test4.text
// Total File Size 24688 bytes
#include <stdio.h>
int main(){
401126: 55 push %rbp
401127: 48 89 e5 mov %rsp,%rbp
printf("Hello World!!!, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d \n", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
40112a: 48 83 ec 08 sub $0x8,%rsp
40112e: 6a 0a pushq $0xa
401130: 6a 09 pushq $0x9
401132: 6a 08 pushq $0x8
401134: 6a 07 pushq $0x7
401136: 6a 06 pushq $0x6
401138: 41 b9 05 00 00 00 mov $0x5,%r9d
40113e: 41 b8 04 00 00 00 mov $0x4,%r8d
401144: b9 03 00 00 00 mov $0x3,%ecx
401149: ba 02 00 00 00 mov $0x2,%edx
40114e: be 01 00 00 00 mov $0x1,%esi
401153: bf 10 20 40 00 mov $0x402010,%edi
401158: b8 00 00 00 00 mov $0x0,%eax
40115d: e8 ce fe ff ff callq 401030 <printf@plt>
401162: 48 83 c4 30 add $0x30,%rsp
401166: b8 00 00 00 00 mov $0x0,%eax
40116b: c9 leaveq
40116c: c3 retq
40116d: 0f 1f 00 nopl (%rax)
If we review the results, we can see all the steps needed to perform a “printf” with ten arguments.
TEST 5
Move the printf()
call to a separate function named output()
$ gcc -g -O0 -fno-builtin -o test5 testFunctionCall.c
// Code
#include <stdio.h>
void output(){
printf("hello World");
}
int main(){
output();
}
$ objdump -fsd --source test5 >> test5.text
// Total File Size 24792 bytes
int main(){
40113c: 55 push %rbp
40113d: 48 89 e5 mov %rsp,%rbp
output();
401140: b8 00 00 00 00 mov $0x0,%eax
401145: e8 dc ff ff ff callq 401126
If we review the results, we can see that we are now using the output function for the “printf.”
TEST 6
Remove -O0
and add -O3
to the gcc options.
$ gcc -g -fno-builtin -O3 -o test6 test.c
// Code
#include <stdio.h>
int main(){
printf("Hello World!\n");
}
$ objdump -fsd --source test6 >> test6.text
// Total File Size 24896 bytes
#include <stdio.h>
int main(){
401040: 48 83 ec 08 sub $0x8,%rsp
printf("Hello World!\n");
401044: bf 10 20 40 00 mov $0x402010,%edi
401049: 31 c0 xor %eax,%eax
40104b: e8 e0 ff ff ff callq 401030 <printf@plt>
401050: 31 c0 xor %eax,%eax
401052: 48 83 c4 08 add $0x8,%rsp
401056: c3 retq
If we review the results, we can see that the -O3
gcc option has swapped a bunch of operations with more efficient versions.