编译与链接流程与符号

声明与定义分离
在C/C++
中,我们编写一段代码,通常会将声明与实现分离。将函数的声明放在*.h
文件中,将函数的实现放在*.cpp
文件或*.c
文件中。这种声明与实现分离的方式可以很方便的避免重复实现带来的冲突问题。
这种冲突问题,更像是一种编译器、链接器的模型设计遗留问题。因为C++
在早期版本中,并不支持模块系统,同一个定义不会有自己的模块作用域约束,则全局无法保证存在哪些函数,且这些函数必须拥有不同的函数签名。
在C++ 20
提出了C++ Modules
的模块化概念,此时有约束需要让每个模块自身保证模块中的函数保持签名唯一,这样就大大降低了重定义问题的出现概率。
符号的概念
符号是用来标识代码中的各种实体的名称。例如,函数名、变量名、类名、类型名等在编译器内部都会被处理为符号。符号的具体表现形式取决于编译器和目标平台,它们在不同的编译阶段扮演不同的角色。我们主要探究函数的符号表现与替换。
链接后的符号表
#include <iostream>void test(){std::cout << "hello" << std::endl;}int add(int a, int b){return a + b;}int main(){ test(); int it = add(2 , 3); return 0;}
g++ -c ./main.cpp -o main.o # 编译&汇编 得到二进制汇编代码 main.o文件nm ./main.o # 通过nm 查看.o文件的符号表

可以看到main.o
文件中已经存在了两个函数的实际地址和符号值。
修改代码,删除add
函数的定义。
#include "iostream"#include <ostream>void test(){std::cout << "hello" << std::endl;}
int add(int a, int b);
int main(){ test(); int it = add(2 , 3); return 0;}
g++ -c ./main.cpp -o main.o # 编译&汇编 得到二进制汇编代码 main.o文件nm ./main.o # 通过nm 查看.o文件的符号表
重新观察符号表:

发现add
函数的符号表对应的地址被设置为了空值。
链接中符号替换为地址
我们知道,汇编中的函数调用形式是call xxxxxxxx
,通过指定一个地址进行跳转流程调用。当我们编写了一段代码:
#include "iostream"void test(){ using namespace std; cout << "hello word!" << endl;}int main(){ test(); return 0;}
g++ ./main.cpp -S ./main.s # 我们对这个文件进行仅编译,查看输出文件的内容
我截取了汇编代码,完整的代码可以看[附录-main.s
内容](# main.s
内容)。
main:.LFB2061: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $32, %rsp .seh_stackalloc 32 .seh_endprologue call __main call _Z4testv movl $0, %eax addq $32, %rsp popq %rbp ret
观察到第11行存在:call _Z4testv
,这个_Z4testv
此时并不是地址,也不是我们定义的原始的函数名称,这是因为C++
的名称修饰的特性,会将函数名称按照函数的完整签名进行一个编码。
当我们删除掉test
函数的定义后:
#include "iostream"void test();int main(){ test(); return 0;}
此时再次进行编译,我们得到了如下的汇编文本代码:
.file "main.cpp" .text .def __main; .scl 2; .type 32; .endef .globl main .def main; .scl 2; .type 32; .endef .seh_proc mainmain:.LFB2060: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $32, %rsp .seh_stackalloc 32 .seh_endprologue call __main call _Z4testv movl $0, %eax addq $32, %rsp popq %rbp ret .seh_endproc .section .rdata,"dr"_ZNSt8__detail30__integer_to_chars_is_unsignedIjEE: .byte 1_ZNSt8__detail30__integer_to_chars_is_unsignedImEE: .byte 1_ZNSt8__detail30__integer_to_chars_is_unsignedIyEE: .byte 1 .ident "GCC: (x86_64-win32-seh-rev1, Built by MinGW-Builds project) 13.2.0" .
但当我们想要继续通过链接来生成可执行文件时,链接器将会报错:
> g++ .\main.cpp -o main.exe
D:/soft/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/13.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:\Users\asahi\AppData\Local\Temp\ccCh64AI.o:main.cpp:(.text+0xe): undefined reference to `test()'collect2.exe: error: ld returned 1 exit status
没能找到test()
函数的实现,导致了链接错误。
由此我们得到了重要的结论:
- 编译时,编译器并不关系函数是否定义,编译器仅仅关心语法(是否声明)。
- 链接时,通过查找所有的符号表,来寻找函数对应的定义,并将编译后的
call [test]
转换为call xxxxxxxx
命令的机器码写入可执行程序的代码段。
静态库与动态库介绍
静态库:在编译链接期间使用的代码,每次修改静态库代码,目标程序都必须再次编译链接产出可执行程序后得到更新。
动态库:在程序运行期间执行的代码,每次修改动态库代码后,目标程序不需要编译链接就能得到更新。
静态库的原理与手工实现
通过符号表、编译链接原理,我们知道,在编译期间我们不需要实现函数,只需要声明一个函数。我们的.o
文件与另一个实现了函数的.o
文件一起进行链接,就可以生成可执行程序。
void test(); // 仅声明int main(){ test(); return 0;}
// lib.cpp#include <iostream>void test(){ // 声明并定义 std::cout << "Hello Lib!" << endl;}
g++ -c lib.cpp -o lib.obj # 编译为二进制文件 g++ main3.cpp .\lib.obj -o test.exe # 先编译再与./lib.obj文件一起链接为 test.exe 可执行程序 ./test.exe # 执行

动态库的加载
动态加载
动态加载的方式是使用windows.h
中提供的函数LoadLibrary
获取模块句柄后再使用GetProcAddress
获取函数指针。
注意:通过
GetProcAddress
获取的函数指针,是普通函数指针。在C++中普通函数想要转换为成员函数的话,需要使用两次强制转换。
静态加载
静态加载的方式是通过链接器指令#pragma comment(lib, "mylib.lib")
导入lib
文件,在程序运行时期,会通过windows系统
默认提供的dll加载路径
来进行查找lib
文件对应的dll
文件。通常两者的名称一致,且重命名并不影响查找的文件名。
附录
main.s
内容-定义后
.file "main.cpp" .text .section .rdata,"dr".LC0: .ascii "hello word!\0" .text .globl _Z4testv .def _Z4testv; .scl 2; .type 32; .endef .seh_proc _Z4testv_Z4testv:.LFB2060: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $32, %rsp .seh_stackalloc 32 .seh_endprologue leaq .LC0(%rip), %rax movq %rax, %rdx movq .refptr._ZSt4cout(%rip), %rax movq %rax, %rcx call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc movq %rax, %rcx movq .refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(%rip), %rax movq %rax, %rdx call _ZNSolsEPFRSoS_E nop addq $32, %rsp popq %rbp ret .seh_endproc .def __main; .scl 2; .type 32; .endef .globl main .def main; .scl 2; .type 32; .endef .seh_proc mainmain:.LFB2061: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $32, %rsp .seh_stackalloc 32 .seh_endprologue call __main call _Z4testv movl $0, %eax addq $32, %rsp popq %rbp ret .seh_endproc .section .rdata,"dr"_ZNSt8__detail30__integer_to_chars_is_unsignedIjEE: .byte 1_ZNSt8__detail30__integer_to_chars_is_unsignedImEE: .byte 1_ZNSt8__detail30__integer_to_chars_is_unsignedIyEE: .byte 1 .ident "GCC: (x86_64-win32-seh-rev1, Built by MinGW-Builds project) 13.2.0" .def _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc; .scl 2; .type 32; .endef .def _ZNSolsEPFRSoS_E; .scl 2; .type 32; .endef .section .rdata$.refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, "dr" .globl .refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ .linkonce discard.refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_: .quad _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ .section .rdata$.refptr._ZSt4cout, "dr" .globl .refptr._ZSt4cout .linkonce discard.refptr._ZSt4cout: .