再谈编译链接
C++函数重载与编译链接-CSDN博客
之前我已经写过文章简单介绍了编译链接要做的一些操作。现在为了能更好的理解我们平时的开发环境,我会在Linux系统上完整地走一遍流程。
环境描述
我们使用普通用户在Linux上进行操作,先写一段测试代码。
[ssddffaa@code code]$ vim test.c#include <stdio.h>int main()
{printf("Hello World!\n");return 0;
}
接下来我们使用 gcc 编译器编译一下我们的文件。
[ssddffaa@code code]$ gcc test.c
[ssddffaa@code code]$ ls
a.out test.c
[ssddffaa@code code]$ ll
total 16
-rwxrwxr-x. 1 ssddffaa ssddffaa 8360 Mar 28 09:32 a.out
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
[ssddffaa@code code]$
可以看到默认生成了一个带可执行权限的文件 a.out ,接着我们运行它。
[ssddffaa@code code]$ ./a.out
Hello World!
要是不想要默认生成的可执行文件,我们也可以使用 gcc 的 "-o" 参数改变 gcc 的输出。
[ssddffaa@code code]$ gcc test.c -o test.txt
[ssddffaa@code code]$ ll
total 28
-rwxrwxr-x. 1 ssddffaa ssddffaa 8360 Mar 28 09:32 a.out
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rwxrwxr-x. 1 ssddffaa ssddffaa 8360 Mar 28 09:36 test.txt[ssddffaa@code code]$ ./test.txt
Hello World!
可以看到我们重新生成了一个可执行文件 test.txt。可能你会疑惑,这个文件的后缀名是 "txt" 怎么还能执行?他不应该是一个记事本嘛?
在Linux系统中文件是否能被运行有两个条件,一是文件具有可执行权限,二是文件本身具有可执行的功能。以上两个条件我们的 text.txt 都满足了,当然可以正常执行。相对的如果一个文件只有可执行权限,但是文件本身本没有可执行能力,那也是执行不了的。
编译链接
预处理
下面我们来看一看 test.c 文件经过预处理后会发生什么。通过 gcc 加 "-E" 参数可以使文件编译到预处理阶段完成后就停止,并把结果打印到屏幕上,使用 "-o" 参数可以重定向到指定文件中。
[ssddffaa@code code]$ gcc -E test.c -o test.i
[ssddffaa@code code]$ ll
total 24
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rw-rw-r--. 1 ssddffaa ssddffaa 16872 Mar 28 09:45 test.i[ssddffaa@code code]$ vim test.i
825 extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
826 # 913 "/usr/include/stdio.h" 3 4
827 extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
828
829
830
831 extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
832
833
834 extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
835 # 943 "/usr/include/stdio.h" 3 4
836
837 # 2 "test.c" 2
838
839 int main()
840 {
841
842 printf("Hello World!\n");
843 return 0;
844 }
当我们进入 test.i 文件中进行查看时,会发现原本我们只写了几行的代码在 test.i 文件中增加到了800多行。我们多出来的这些代码是什么呢?我想你应该已经猜到了,这是预处理阶段要做的一件事 "头文件展开"。没错,多出来的这几百行代码就是 #include <stdio.h> 的效果,那么这些代码是凭空出现的嘛,显然不是。头文件展开其实就是去把Linux系统中的头文件内容拷贝了一份然后放进了我们的代码。
我们可以在系统中找一下存放头文件的位置。
[ssddffaa@code code]$ sudo find / -name stdio.h
[sudo] password for ssddffaa:
/usr/include/c++/4.8.2/tr1/stdio.h
/usr/include/bits/stdio.h
/usr/include/stdio.h
出现了3个路径不过只有最下面那个才是我们使用的头文件,这一点可以通过查看 gcc 的默认include路径了解。
[ssddffaa@code include]$ `gcc -print-prog-name=cc1plus` -v
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../include/c++/4.8.5/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../include/c++/4.8.5/x86_64-redhat-linux/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../include/c++/4.8.5/backward/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/usr/local/include/usr/include
End of search list.
可以看到,我们包含的头文件确确实实是存在在我们的计算机上的。平时我们使用的 printf 等函数,并没有我们自己实现但是我们就在使用了,这是因为我们包含了头文件,然后在预处理时编译器会把我们包含的头文件内容拷贝到我们写的代码里,然后在之后的编译阶段编译器才能识别到 printf 等库函数接着进行下一步操作。
预处理阶段不仅仅只是展开头文件还有以下内容。
#1 将#define删除,并且展开所有的宏定义
#2 处理条件编译,#if #ifdef #elif #endif等
#3 头文件展开
#4 删除注释
#5 添加行号和文件标识,以便编译时产生调试试用的行号以及编译错误警告号
#6 保留所有的#pragma编译器指令,因为编译器需要使用它们
编译
预处理完成后会进入编译阶段,在这个阶段我们之前处理过的 test.i 文件会进一步进行处理。编译阶段在整个编译链接中是最复杂的一个。
编译过程可以分为6步:
#1 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)
#2 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)
#3 语义分析:静态语义(编译器可以确定的语义),动态语义(只能在运行期才能确定的语义)
#4 源代码优化:源代码优化器,将整个语法树转换为中间代码(中间代码是与目标机器和运行环境无关的)中间使得编译器被分为前端和后端,编译器前端负责产生机器无关的中间代码编译器后端将中间代码转化为目标机器代码
#5 目标代码生成:代码生成器(Code Generator)
#6 目标代码优化:目标代码优化器(Target Code Optimizer)
我们代码语法的检查,代码的优化等都是在编译阶段完成的。在编译阶段还会生成符号表,它用于链接阶段。
使用 gcc 加 "-S" 参数可以让文件编译执行到编译阶段完成后终止。
[ssddffaa@code code]$ gcc -S test.c -o test.s
[ssddffaa@code code]$ ll
total 28
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rw-rw-r--. 1 ssddffaa ssddffaa 16872 Mar 28 09:45 test.i
-rw-rw-r--. 1 ssddffaa ssddffaa 448 Mar 28 20:23 test.s[ssddffaa@code code]$ vim test.s1 .file "test.c"2 .section .rodata3 .LC0:4 .string "Hello World!"5 .text6 .globl main7 .type main, @function8 main:9 .LFB0:10 .cfi_startproc11 pushq %rbp12 .cfi_def_cfa_offset 1613 .cfi_offset 6, -1614 movq %rsp, %rbp15 .cfi_def_cfa_register 616 movl $.LC0, %edi17 call puts18 movl $0, %eax19 popq %rbp20 .cfi_def_cfa 7, 821 ret22 .cfi_endproc23 .LFE0:24 .size main, .-main25 .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"26 .section .note.GNU-stack,"",@progbits
可以看到 test.s 文件里是汇编代码。
汇编阶段
在汇编阶段,汇编器将 test.s 文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位二进制目标程序的格式,并将结果保存在目标文件 test.o 中,test.o 是一个二进制文件。
使用 gcc 加 "-c" 参数可以让文件编译执行到汇编阶段完成后终止。
[ssddffaa@code code]$ gcc -c test.c -o test.o
[ssddffaa@code code]$ ll
total 32
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rw-rw-r--. 1 ssddffaa ssddffaa 16872 Mar 28 09:45 test.i
-rw-rw-r--. 1 ssddffaa ssddffaa 1496 Mar 28 20:40 test.o
-rw-rw-r--. 1 ssddffaa ssddffaa 448 Mar 28 20:33 test.s[ssddffaa@code code]$ vim test.o
使用 vim 文本编辑器进入 test.o 查看会全是乱码,这是因为 test.o 中是二进制数据,而 vim 只能识别文本,所以我们换一个工具进行查看。
[ssddffaa@code code]$ hexdump test.o
可以看到文件由二进制转为16进制的内容。我们的计算机能识别二进制,而我们的 test.o 文件就是一个二进制文件那我们可以不可以运行它呢?我们来试一下。
[ssddffaa@code code]$ ll
total 32
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rw-rw-r--. 1 ssddffaa ssddffaa 16872 Mar 28 09:45 test.i
-rw-rw-r--. 1 ssddffaa ssddffaa 1496 Mar 28 20:40 test.o
-rw-rw-r--. 1 ssddffaa ssddffaa 448 Mar 28 20:33 test.s
[ssddffaa@code code]$ chmod +x test.o
[ssddffaa@code code]$ ll
total 32
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rw-rw-r--. 1 ssddffaa ssddffaa 16872 Mar 28 09:45 test.i
-rwxrwxr-x. 1 ssddffaa ssddffaa 1496 Mar 28 20:40 test.o
-rw-rw-r--. 1 ssddffaa ssddffaa 448 Mar 28 20:33 test.s
[ssddffaa@code code]$ ./test.o
-bash: ./test.o: cannot execute binary file
可以看到报了错误 "无法执行的二进制文件"。汇编生成的二进制文件还不具有可执行的能力,这是因为缺少了库文件。我们在预处理阶段包含的头文件只是声明,声明和定义是分离的,定义是存储在库文件中的,同时库中还包含了许多方法。
链接
汇编过后生成的目标文件(*.o)并不是最终的可执行二进制文件,而是一种中间文件(或临时文件),目标文件任然需要经过链接才能变成可执行文件。
在链接阶段,链接器会把我们写的所有目标文件(*.o)与系统组件(标准库,动态链接库等)结合起来生成一个可执行程序。(链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件)。
[ssddffaa@code code]$ gcc test.c -o test
[ssddffaa@code code]$ ll
total 44
-rwxrwxr-x. 1 ssddffaa ssddffaa 8360 Mar 28 21:47 test
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rw-rw-r--. 1 ssddffaa ssddffaa 16872 Mar 28 09:45 test.i
-rw-rw-r--. 1 ssddffaa ssddffaa 1496 Mar 28 20:40 test.o
-rw-rw-r--. 1 ssddffaa ssddffaa 448 Mar 28 20:33 test.s
[ssddffaa@code code]$ ./test
Hello World!
Linux下的C语言的标准库文件在 "/usr/lib64/" 下本质就是一个文件。
[ssddffaa@code code]$ ll /usr/lib64/libc.so*
-rw-r--r--. 1 root root 253 May 18 2022 /usr/lib64/libc.so
lrwxrwxrwx. 1 root root 12 Mar 27 08:53 /usr/lib64/libc.so.6 -> libc-2.17.so
Linux和Windows下的库文件。
# Linux: .so(动态库) .a(静态库)
# Windows .dll(动态库) .lib(静态库)库的命名规则
# libname.so.XXX
# 其中lib是库前缀名,name则是库的名称,so是库后缀,XXX是版本号
编译型语言,安装开发包,必定是下载安装对应的头文件和库文件。
库其实就是把源文件(.c),经过一定的翻译,然后打包形成一个文件。不用给你提供太多的源文件,也可以达到隐藏源文件的目的。
头文件提供声明,库文件提供实现 + 你的代码 = 你的软件。
动态链接与静态链接
库有动态库和静态库,链接也有动态链接和静态链接。
动态链接
动态库是一个共享的库文件,当程序开始执行时编译器便会告诉程序动态库在哪个位置,有哪些方法。当程序执行到需要库里的文件时便会跑到库里去执行,执行完又继续执行下一步操作。像这样编译器告诉程序动态库的位置,然后程序需要时跑去执行的方式叫动态链接。
动态库不能缺失,一旦对应的动态库缺失,影响的不止一个程序,可能导致很多程序都无法正常运行。
静态链接
静态链接就相当于直接把静态库中要用到的内容直接和我们的目标文件链接在一起生成可执行程序。完成后,就算静态库没了程序也能正常运行。
比较
动态链接和静态链接各有各的优势,当一个库文件有许多程序都需要使用时,用动态链接可以让程序不用去每个都拷贝一份库文件。而且更新库文件时方便,你只需要改库文件就可以完成库的更新。
静态链接可以让程序不依赖之前的静态库文件,减少重定向链接到动态库的开销提高了程序效率。但是这个效率只提高了5%左右。更新库时麻烦,需要重新修改静态库文件,然后再编译链接一次生成新的可执行文件。
验证
使用 "ldd" 可以查看程序的动态链接情况
[ssddffaa@code code]$ ldd testlinux-vdso.so.1 => (0x00007ffc36b9c000)libc.so.6 => /lib64/libc.so.6 (0x00007f4ff73f3000)/lib64/ld-linux-x86-64.so.2 (0x00007f4ff77c1000)
可以看到,在Linux下 gcc 默认使用的是动态链接。 在编译时加上 "-static" 参数可以让 gcc 使用静态链接。
[ssddffaa@code code]$ gcc test.c -o test-static -static
/usr/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status
报错了,这个报错的原因呢是因为我们的Linux系统上默认是没有静态库的,需要我们手动去安装一下。
[ssddffaa@code code]$ sudo yum install -y glibc-static
[ssddffaa@code code]$ sudo yum install -y libstdc++-static
安装完成后我们再去试一下。
[ssddffaa@code code]$ gcc test.c -o test-static -static
[ssddffaa@code code]$ ll -h
total 888K
-rwxrwxr-x. 1 ssddffaa ssddffaa 8.2K Mar 28 21:47 test
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rw-rw-r--. 1 ssddffaa ssddffaa 17K Mar 28 09:45 test.i
-rw-rw-r--. 1 ssddffaa ssddffaa 1.5K Mar 28 20:40 test.o
-rw-rw-r--. 1 ssddffaa ssddffaa 448 Mar 28 20:33 test.s
-rwxrwxr-x. 1 ssddffaa ssddffaa 842K Mar 29 01:41 test-static
[ssddffaa@code code]$ ldd test-static not a dynamic executable
可以看到成功生成了一个可执行程序 test-static,并且它没有使用任何动态库。但是 test-static 和 test 的大小起来大了80K左右。
#1 如果我们没有静态库,但是我们就要-static,是不行的
#2 如果我们没有动态库,只有静态库,而且gcc能找到不使用-staticgcc还是会链接成功,并且使用静态库这是因为gcc默认使用动态链接,使用-static只是增加了静态链接的优先级
#3 不一定是纯的全部动态链接或静态链接,是混合的
查看可执行程序使用的库情况。
[ssddffaa@code code]$ file test
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=9fc19efe5a60448b3a2d19e1e150f7b77d65993a, not stripped
[ssddffaa@code code]$ file test-static
test-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=7cd92dd71613a6bad6eea55cc7d520b28e98e9d7, not stripped
Debug && Release
Debug 模式相比 Release 模式增加了调试信息,可以方便开发人员对代码进行调试。gcc 默认使用的是 Release 模式,要使用 Debug 模式需要加上参数 "-g"。
[ssddffaa@code code]$ gcc test.c -o test_debug -g
[ssddffaa@code code]$ gcc test.c -o test-static_debug -static -g
[ssddffaa@code code]$ ll
total 1716
-rwxrwxr-x. 1 ssddffaa ssddffaa 8360 Mar 28 21:47 test
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rwxrwxr-x. 1 ssddffaa ssddffaa 9360 Mar 29 02:09 test_debug
-rwxrwxr-x. 1 ssddffaa ssddffaa 861288 Mar 29 01:41 test-static
-rwxrwxr-x. 1 ssddffaa ssddffaa 862296 Mar 29 02:10 test-static_debug
可以看到 Debug 模式生成的可执行程序都比 Release 模式下生成的可执行程序大,这是因为 Debug 模式增加了调试信息。
[ssddffaa@code code]$ readelf -S test_debug | grep debug[27] .debug_aranges PROGBITS 0000000000000000 00001061[28] .debug_info PROGBITS 0000000000000000 00001091[29] .debug_abbrev PROGBITS 0000000000000000 00001122[30] .debug_line PROGBITS 0000000000000000 00001164[31] .debug_str PROGBITS 0000000000000000 0000119f
make/Makefile
一个工程中的源文件不计其数,按类型、功能、模块分别放在若干个目录中,Makefile 定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
make是一条指令,Makefile是一个当前目录下的文件。
[ssddffaa@code code]$ touch Makefile
[ssddffaa@code code]$ vim Makefile1 test.exe:test.c #编写 Makefile 第一行是依赖关系2 gcc -o test.exe test.c #第二行是以TAB开始然后输入依赖方法。3 .PHONY:clean #加上PHONY参数修饰clean4 clean: #清理5 rm -f test.exe #清理方法
通过 make 指令来执行 Makefile 文件内容。
[ssddffaa@code code]$ make
gcc -o test.exe test.c
[ssddffaa@code code]$ ll
total 20
-rw-rw-r--. 1 ssddffaa ssddffaa 40 Mar 29 02:41 Makefile
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
-rwxrwxr-x. 1 ssddffaa ssddffaa 8360 Mar 29 02:41 test.exe
[ssddffaa@code code]$ ./test.exe
Hello World!
[ssddffaa@code code]$ make clean
rm -f test.exe
[ssddffaa@code code]$ ll
total 8
-rw-rw-r--. 1 ssddffaa ssddffaa 76 Mar 29 02:50 Makefile
-rw-rw-r--. 1 ssddffaa ssddffaa 75 Mar 28 09:31 test.c
可以看出 make/Makefile 能把我们从冗余的 gcc 命令中解脱出来,只需 make 和 make clean 就可以构建和删除项目。
参考文章
程序详细编译过程(预处理、编译、汇编、链接) - 知乎 (zhihu.com)
详解C/C++代码的预处理、编译、汇编、链接全过程 - 知乎 (zhihu.com)
深入浅出静态链接和动态链接-CSDN博客