这是本人的第一篇博客,主要是想记录一些心得,增加印象,如果能给大家提供一些参考就更好了。
水平有限,还请大家批判。
本文全部例子在centos 7上运行,gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC)
一个例子(文件名为 hello_damon.c):
#include <stdio.h>
#ifndef KKK
#pragma message("not define KKK...")
#define ONE_K 1024
#endif
//I am annotation
/*
* I am annotation too
*/
int main(int argc, char **argv)
{
printf("hello, damon\n");
printf("ONE_K = %d\n", ONE_K);
return 0;
}
编译以及运行的结果 :
[damon@localhost work]$ ls
hello_damon.c
[damon@localhost work]$ gcc hello_damon.c
hello_damon.c:4:10: 附注:#pragma message:not define KKK...
#pragma message("not define KKK...")
^
[damon@localhost work]$ ./a.out
hello, damon
ONE_K = 1024
以上例子虽然简单,但gcc hello_damon.c 这个命令背后究竟发生了什么?
为何在此之后程序可以被编译成为一个可执行文件?
正片开始:
gcc hello_damon.c 这个命令可以被分解为如下四步:
一:预处理(也叫做 预编译)
gcc -E hello_damon.c -o hello_damon.i
在执行命令之后我们得到hello_damon.i文件,文本较多,我截取其中一部分来说明问题
.......
.......
# 4 "hello_damon.c"
#pragma message("not define KKK...")
# 4 "hello_damon.c"
# 14 "hello_damon.c"
int main(int argc, char **argv)
{
printf("hello, damon\n");
printf("ONE_K = %d\n", 1024);
return 0;
}
在这个文件中我们可以看到
1. # 4 "hello_damon.c" 以及 # 14 "hello_damon.c" 是行号和文件名标识,用于编译器产生调试用的行号以及编译错误或警告时用的错误信息。
2.所有的“#define"被删除,并且展开所有的宏定义,在文件中可以看到printf("ONE_K = %d\n", ONE_K); 被替换为 printf("ONE_K = %d\n", 1024);
3.所有的条件编译信息被处理( #ifndef #endif等 )
4.#pragma 编译器指令被保留
5.“#include” 指令被处理,该处理是递归的,我在例子仅仅只包含了"<stdio.h>"但是产生的hello_damon.i还是很大,因为"<stdio.h>"还包含其头文件
6.注释被处理( // 以及 /**/)
二:编译
gcc -S hello_damon.i -o hello_damon.s
在执行命令之后我们得到hello_damon.s 汇编代码 (注意尾缀是小s,和大写S是有区别的),如下
.file "hello_damon.c"
.section .rodata
.LC0:
.string "hello, damon"
.LC1:
.string "ONE_K = %d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl $.LC0, %edi
call puts
movl $1024, %esi
movl $.LC1, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
.section .note.GNU-stack,"",@progbits
三:汇编
gcc -c hello_damon.s -o hello_damon.o
执行该命令之后我们得到目标文件hello_damon.o,该文件已经是二进制机器码不能直接查看,此时可以借助如下命令
objdump -t hello_damon.o
得到输出:
hello_damon.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 hello_damon.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g F .text 0000000000000034 main
0000000000000000 *UND* 0000000000000000 puts
0000000000000000 *UND* 0000000000000000 printf
我们可以在此处直观的查看到函数信息等。
四:链接
ld -static crt1.o crti.o crtbeginT.o -start-group -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o hello_damon.o
注意上述命令中的crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o目标文件是需要输入全路径的,此处未用全路径,取决于不同的机器环境,是gcc加入的系统标准启动文件,对于一般应用程序,这些启动是必需的。
简单的说:链接就是将汇编生成的OBJ文件、系统库的OBJ文件、库文件链接起来,最终生成可以在特定平台运行的可执行程序。
链接的细节以及过程相对复杂,我准备另开一篇进行记录总结。
总结:
编译过程可以分为四个步骤:
预处理->编译->汇编->链接
预处理:gcc -E hello_damon.c -o hello_damon.i
编译: gcc -S hello_damon.i -o hello_damon.s
汇编: gcc -c hello_damon.s -o hello_damon.o
链接: ld -static crt1.o crti.o crtbeginT.o -start-group -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o hello_damon.o
参考:
《程序员的自我修养》https://book.douban.com/subject/3652388/
gcc程序的编译过程和链接原理 https://blog.csdn.net/czg13548930186/article/details/78331692
水平有限,不足与错误之处,还请大家多多指教