一、编译C代码:
源文件:
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
编译命令:
[root]# gcc hello.c
编译hello.c,生成最终二进制可执行程序,未指定输出文件名时,默认输出为a.out。
这条命令隐含执行了GCC编译C源码的四个步骤:
预处理—–> 编译 —–> 汇编 —–> 链接
形成最终的二进制可执行程序。
二、根据GCC的命令选项剖析GCC执行过程。
1)预处理(Pre-processing)
编译器将C源代码中包含的头文件(如stdio.h)编译进来,使用”-E”选项。
用法:
[root]# gcc -E hello.c -o hello.i
作用:将hello.c预处理输出hello.i文件。
查看hello.i文件:
1 # 1 "hello.c"
2 # 1 "<built-in>"
3 # 1 "<command-line>"
4 # 1 "hello.c"
5 # 1 "/usr/include/stdio.h" 1 3 4
6 # 28 "/usr/include/stdio.h" 3 4
7 # 1 "/usr/include/features.h" 1 3 4
8 # 361 "/usr/include/features.h" 3 4
9 # 1 "/usr/include/sys/cdefs.h" 1 3 4
10 # 365 "/usr/include/sys/cdefs.h" 3 4
11 # 1 "/usr/include/bits/wordsize.h" 1 3 4
12 # 366 "/usr/include/sys/cdefs.h" 2 3 4
13 # 362 "/usr/include/features.h" 2 3 4
14 # 385 "/usr/include/features.h" 3 4
15 # 1 "/usr/include/gnu/stubs.h" 1 3 4
16
17
18
19 # 1 "/usr/include/bits/wordsize.h" 1 3 4
20 # 5 "/usr/include/gnu/stubs.h" 2 3 4
。。。省略。。。
836 extern char *ctermid (char *__s) __attribute__ ((__nothrow__));
837 # 908 "/usr/include/stdio.h" 3 4
838 extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__));
839
840
841
842 extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;
843
844
845 extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));
846 # 938 "/usr/include/stdio.h" 3 4
847
848 # 2 "hello.c" 2
849
850 int main(void)
851 {
852 printf("Hello World!\n");
853
854 return 0;
855 }
2)编译阶段(Compiling)
gcc检查代码规范性、语法错误等,以确定代码实际要做的工作,检查无误后,gcc把代码翻译成汇编语言。使用”-S”选项,只进行编译不进行汇编,生成汇编代码。
用法:
[root]# gcc –S hello.i –o hello.s
作用:将预处理输出文件hello.i汇编成hello.s文件。
[root@richard hello-gcc]# ls
hello.c hello.i hello.s
查看hello.s文件:
1 .file "hello.c"
2 .section .rodata
3 .LC0:
4 .string "Hello World!"
5 .text
6 .globl main
7 .type main, @function
8 main:
9 .LFB0:
10 .cfi_startproc
11 pushq %rbp
12 .cfi_def_cfa_offset 16
13 .cfi_offset 6, -16
14 movq %rsp, %rbp
15 .cfi_def_cfa_register 6
16 movl $.LC0, %edi
17 call puts
18 movl $0, %eax
19 leave
20 .cfi_def_cfa 7, 8
21 ret
22 .cfi_endproc
23 .LFE0:
24 .size main, .-main
25 .ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-3)"
26 .section .note.GNU-stack,"",@progbits
3)汇编阶段(Assembling)
把编译阶段生成的”.s”文件转成二进制目标代码。使用”-c”选项。
用法:
[root]# gcc –c hello.s –o hello.o
作用:将汇编输出文件test.s编译输出test.o文件。
[root]# ls
hello.c hello.i hello.o hello.s
此时的.o文件如果用less命令或vim编辑器查看的话,就是乱码了。
使用nm命令查看目标文件的符号列表:
[root]# nm -C hello.o
0000000000000000 T main
U puts
4)链接阶段(Link)
在成功编译之后,就进入了链接阶段。链接无选项。
用法:
[root]# gcc hello.o –o hello
作用:将编译输出文件hello.o链接成最终可执行文件hello。
[root]# ls
hello.c hello hello.i hello.o hello.s
使用nm命令查看最终可执行文件的符号列表:
[root]# nm -C hello
00000000006006b8 d _DYNAMIC
0000000000600850 d _GLOBAL_OFFSET_TABLE_
00000000004005c8 R _IO_stdin_used
w _Jv_RegisterClasses
0000000000600698 d __CTOR_END__
0000000000600690 d __CTOR_LIST__
00000000006006a8 D __DTOR_END__
00000000006006a0 d __DTOR_LIST__
0000000000400688 r __FRAME_END__
00000000006006b0 d __JCR_END__
00000000006006b0 d __JCR_LIST__
000000000060087c A __bss_start
0000000000600878 D __data_start
0000000000400580 t __do_global_ctors_aux
0000000000400430 t __do_global_dtors_aux
00000000004005d0 R __dso_handle
w __gmon_start__
000000000060068c d __init_array_end
000000000060068c d __init_array_start
00000000004004e0 T __libc_csu_fini
00000000004004f0 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
000000000060087c A _edata
0000000000600890 A _end
00000000004005b8 T _fini
0000000000400390 T _init
00000000004003e0 T _start
000000000040040c t call_gmon_start
0000000000600880 b completed.6349
0000000000600878 W data_start
0000000000600888 b dtor_idx.6351
00000000004004a0 t frame_dummy
00000000004004c4 T main
U puts@@GLIBC_2.2.5
A Global absolute 符号。
a Local absolute 符号。
B Global bss 符号。
b Local bss 符号。
D Global data 符号。
d Local data 符号。
f 源文件名称符号。
R 该符号位于只读数据区。
T Global text 符号。
t Local text 符号。
U 未定义符号。在这个库里面使用了,但是在其他库中定义的符号;
W 如果其他函数库中也有该符号定义,则其他符号的定义可覆盖该定义。
查看”puts@@GLIBC_2.2.5”这个函数在什么库中定义的:
[root@localhost hello]# nm -o /lib/* /usr/lib/* /usr/lib/*/* /usr/local/lib/* 2> /dev/null |grep 'puts@@GLIBC_2.2.5'
/usr/local/lib/libpcap.so: U puts@@GLIBC_2.2.5
/usr/local/lib/libpcap.so.1: U puts@@GLIBC_2.2.5
/usr/local/lib/libpcap.so.1.5.3: U puts@@GLIBC_2.2.5
可将上面四步合成为一步:
[root]# gcc hello.c –o hello
5)运行该可执行文件,出现正确的结果如下。
[root@localhost Gcc]# ./hello
Hello World!
三、一个重要的概念:函数库。
在这个程序中并没有定义“printf”的函数实现,且在预编译中包含进的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现“printf”函数的呢?
答案是:系统把这些函数实现都做到名为libc.so.6的库文件中去了,在没有特别指定时,gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到 libc.so.6库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。
使用ldd命令查看动态库加载情况:
[root]# ldd hello
libc.so.6 => /lib/tls/libc.so.6 (0x42000000)
/lib/ld-Linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
函数库一般分为静态库和动态库两种。
1)静态库:指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时就不再需要库文件了。其后缀名一般为”.a”。可以用ar这个程序来产生静态函数库文件。
ar rcs my_library.a file1.o file2.o
作用:把目标代码file1.o和file2.o加入到my_library.a这个函数库文件中,如果my_library.a不存在则创建一个新文件。
2)动态库:与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件来加载库,以节省系统开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。
将这几个文件编译成一个动态库:libhello.so
$ gcc hello_a.c hello_b.c hello_c.c -fPIC -shared -o libhello.so
将hello.c与动态库libhello.so链接生成可执行文件hello:
$ gcc hello.c -L. -lhello -o hello
GCC命令行编译参数解析:
-shared:指定生成动态连接库(让连接器生成T类型的导出符号表,有时也生成弱连接W类型的导出符号),不用该标志则外部程序无法连接。
-fPIC:编译为位置独立的代码,动态载入时是通过代码拷贝的方式来满足不同进程的需要,但不能达到真正代码段共享的目的。
-L.:表示要连接的库在当前目录中
-lhello:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称
四、关于gcc控制输出类型的选项:
Compilation can involve up to four stages: preprocessing, compilation proper, assembly and linking, always in that order. GCC is capable of preprocessing and compiling several files either into several assembler input files, or into one assembler input file; then each assembler input file produces an object file, and linking combines all the object files (those newly compiled, and those specified as input) into an executable file.
For any given input file, the file name suffix determines what kind of compilation is done:
file.c
C source code that must be preprocessed.
file.i
C source code that should not be preprocessed.
file.h
C, C++, Objective-C or Objective-C++ header file to be turned into a precompiled header (default), or C, C++ header file to be turned into an Ada spec (via the -fdump-ada-spec switch).
file.s
Assembler code.
-c
Compile or assemble the source files, but do not link. The linking stage simply is not done. The ultimate output is in the form of an object file for each source file.
By default, the object file name for a source file is made by replacing the suffix ‘.c’, ‘.i’, ‘.s’, etc., with ‘.o’.
Unrecognized input files, not requiring compilation or assembly, are ignored.
-S
Stop after the stage of compilation proper; do not assemble. The output is in the form of an assembler code file for each non-assembler input file specified.
By default, the assembler file name for a source file is made by replacing the suffix ‘.c’, ‘.i’, etc., with ‘.s’.
Input files that don't require compilation are ignored.
-E
Stop after the preprocessing stage; do not run the compiler proper. The output is in the form of preprocessed source code, which is sent to the standard output.
Input files that don't require preprocessing are ignored.
-o file
Place output in file file. This applies to whatever sort of output is being produced, whether it be an executable file, an object file, an assembler file or preprocessed C code.
If -o is not specified, the default is to put an executable file in a.out, the object file for source.suffix in source.o, its assembler file in source.s, a precompiled header file in source.suffix.gch, and all preprocessed C source on standard output.
详细请参考:https://gcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/Overall-Options.html#Overall-Options