[转] 程序的编译链接过程
还是从HelloWorld开始说吧...
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("Hello World!\n");
return 0;
}
从源文件Hello.cpp编译链接成Hello.exe,需要经历如下步骤:
可使用以下命令,直接从源文件生成可执行文件
linux:
gcc -lstdc++ Hello.cpp -o Hello.out // 要带上lstdc参数,否则会报undefined reference to '__gxx_personality_v0'错误
g++ Hello.cpp -o Hello.out
注:后缀为.c的文件gcc把它当做c代码,而g++当做c++代码;gcc与g++都是调用器,最终调用的编译器为cc1(c代码),cc1plus(c++c代码)。
另外,链接阶段gcc不会自动和c++标准库链接,需要带上-lstdc++参数才能链接。
windows:
cl Hello.cpp /link -out:Hello.exe
预处理:主要是做一些代码文本的替换工作。(该替换是一个递归逐层展开的过程。)
(1)将所有的#define删除,并展开所有的宏定义
(2)处理所有的条件预编译指令,如:#if #ifdef #elif #else #endif
(3)处理#include预编译指令,将被包含的文件插进到该指令的位置,这个过程是递归的
(4)删除所有的注释//与/* */
(5)添加行号与文件名标识,以便产生调试用的行号信息以及编译错误或警告时能够显示行号
(6)保留所有的#pragma编译器指令,因为编译器需要使用它们
linux:
cpp Hello.cpp > Hello.i
gcc -E Hello.cpp -o Hello.i
g++ -E Hello.cpp -o Hello.i
行号与文件名标识解释:
# 32 "/usr/include/bits/types.h" 2 3 4 // 表示下面行为types.h的第32行
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
以上,#行的行末的数字2 3 4的含义:
1 - 打开一个新文件
2 - 返回上一层文件
3 - 以下的代码来自系统文件
4 - 以下的代码隐式地包裹在extern "C"中
不产生行号与文件名标识:
cpp -P Hello.cpp > Hello.i
gcc -E -P Hello.cpp -o Hello.i
g++ -E -P Hello.cpp -o Hello.i
windows:
cl /E Hello.cpp > Hello.i
行号与文件名标识解释:
#line 283 "C:\\Program Files\\Microsoft Visual Studio\\VC98\\include\\stdio.h" // 表示下面行为stdio.h的第283行
void __cdecl clearerr(FILE *);
int __cdecl fclose(FILE *);
int __cdecl _fcloseall(void);
不产生行号与文件名标识:
cl /EP Hello.cpp > Hello.i
编译:把预处理完的文件进行一系列词法分析(lex)、语法分析(yacc)、语义分析及优化后生成汇编代码,这个过程是程序构建的核心部分。
linux:
/usr/lib/gcc/i586-suse-linux/4.1.2/cc1 Hello.cpp
使用cc1生成出来的Hello.s文件如下(由于Hello.cpp中没有c++的特性,因此也可以用c语言编译器进行编译):
.file "Hello.cpp"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $4, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
addl $4, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.1.2 20070115 (prerelease) (SUSE Linux)"
.section .note.GNU-stack,"",@progbits
对于含c++的特性的cpp文件,应使用cc1plus进行编译,或使用gcc命令来编译(会通过后缀名来选择调用cc1还是cc1plus)
/usr/lib/gcc/i586-suse-linux/4.1.2/cc1plus Hello.cpp
gcc -S Hello.cpp -o Hello.s
g++ -S Hello.cpp -o Hello.s
windows:
cl /FA Hello.cpp Hello.asm
vc6生成出来的Hello.asm文件如下:
TITLE Hello.cpp
.386P
include listing.inc
if @Version gt 510
.model FLAT
else
_TEXT SEGMENT PARA USE32 PUBLIC 'CODE'
_TEXT ENDS
_DATA SEGMENT DWORD USE32 PUBLIC 'DATA'
_DATA ENDS
CONST SEGMENT DWORD USE32 PUBLIC 'CONST'
CONST ENDS
_BSS SEGMENT DWORD USE32 PUBLIC 'BSS'
_BSS ENDS
_TLS SEGMENT DWORD USE32 PUBLIC 'TLS'
_TLS ENDS
FLAT GROUP _DATA, CONST, _BSS
ASSUME CS: FLAT, DS: FLAT, SS: FLAT
endif
PUBLIC _main
EXTRN _printf:NEAR
_DATA SEGMENT
$SG579 DB 'Hello World!', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
_main PROC NEAR
; File Hello.cpp
; Line 7
push ebp
mov ebp, esp
; Line 8
push OFFSET FLAT:$SG579
call _printf
add esp, 4
; Line 9
xor eax, eax
; Line 10
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END
汇编:汇编代码->机器指令。
linux:
as Hello.s -o Hello.o
gcc -c Hello.cpp -o Hello.o
g++ -c Hello.cpp -o Hello.o
windows:
cl /c Hello.cpp > Hello.obj
至此,产生的目标文件在结构上已经很像最终的可执行文件了。
链接:这里讲的链接,严格说应该叫静态链接。多个目标文件、库->最终的可执行文件(拼合的过程)。
可执行文件分类:
linux的ELF文件 -- bin、a、so
windows的PE文件 -- exe、lib、dll
注:PE文件与ELF文件都是COFF文件的变种
linux:
ld -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i586-suse-linux/4.1.2/crtbeginT.o -L/usr/lib/gcc/i586-suse-linux/4.1.2/ -L/usr/lib -L/lib Hello.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/i586-suse-linux/4.1.2/crtend.o /usr/lib/crtn.o -o Hello.out
注:-static:强制所有的-l选项使用静态链接; -L:链接外部静态库与动态库的查找路径;
-l:指定静态库的名称(最后库的文件名为:libgcc.a、libgcc_eh.a、libc.a);
--start-group ... --end-group:之间的内容只能为文件名或-l选项;为了保证内容项中的符号能被解析,链接器会在所有的内容项中循环查找。
这种用法存在性能开销,最好是当有两个或两个以上内容项之间存在有循环引用时才使用。
windows:
link /subsystem:console /out:Hello.exe Hello.obj
静态库本质上就是包含一堆中间目标文件的压缩包,就像zip等文件一样,里面的各个中间文件包含的外部符号地址是没有被链接器修正的。
查看静态库中的内容
linux:
ar -t libc.a
windows:
lib /list libcmt.lib
解压静态库中的内容
linux:【将libc.a中所有的o文件解压到当前目录下】
ar -x /usr/lib/libc.a
windows:【将libcmt.lib中的atof.obj解压到当前目录下】
lib libcmt.lib /extract:build\intel\mt_obj\atof.obj
生成静态库
linux:
ar -rf test.a main.o fun.o
windows:
lib /out:test.lib main.obj fun.obj
符号(Symbol) -- 链接的接口
每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。
在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名,函数或变量的地址就是符号值。
每一个目标文件都有一个符号表,符号有以下几种:
(1) 定义在本目标文件的全局符号,可被其他目标文件引用
如:全局变量,全局函数
(2) 在本目标文件中引用的全局符号,却没有定义在本目标文件 -- 外部符号(External Symbol)
如:extern变量,printf等库函数,其他目标文件中定义的函数
(3) 段名,这种符号由编译器产生,其值为该段的起始地址
如:目标文件的.text、.data等
(4) 局部符号,内部可见
如:static变量
链接过程中,比较关心的是上面的第一类与第二类。
查看符号
linux:
nm Hello.o
readelf -s Hello.o
objdump -t Hello.obj
windows上可以安装MinGW来获取这些工具。
windows:
dumpbin /symbols Hello.obj
符号修饰(Name Decoration)
符号修饰实际就是对变量或函数进行重命名的过程,影响命名的因素有:
(1) 语言的不同,修饰规则有差别
如:foo函数,在C语言中会被修饰成_foo,在Fortran语言中会被修饰成_foo_
(2) 面向对象语言(如:C++)引入的特性
如:类、继承、虚机制、重载、命名空间(namespace)等
函数签名(Function Signature)
函数签名用于识别不同的函数,包括函数名、它的参数类型及个数、所在的类和命名空间、调用约定类型及其他信息
Visual C++的符号修饰与函数签名的规则没有对外公开,但Microsoft提供了一个UnDecorateSymbolName的API,可以将修饰后名称转换成函数原型
使用extern "C",强制C++编译器用C语言的规则来进行符号修饰
extern "C" int g_nTest1;
extern "C" int fun();
#ifdef __cplusplus
extern "C"
{
#endif
int g_nTest2 = 0;
int add(int a, int b);
#ifdef __cplusplus
}
#endif
弱符号与强符号 [wiki]
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
GCC可以通过"__attribute__((weak))"来定义任何一个强符号为弱符号。
extern int __attribute__((weak)) ext; // 将变量ext修改成一个弱符号
int __attribute__((weak)) fun1(); // 将函数fun1修改成一个弱符号
int fun2() __attribute__((weak)); // 将函数fun2修改成一个弱符号
int weak1;
int strong = 1;
int __attribute__((weak)) weak2 = 2; // 强制变量weak2为弱符号
int main()
{
return 0;
}
以上,weak1与weak2是弱符号,strong与main是强符号。
针对强弱符号的概念,链接器会按照以下规则处理与选择被多次定义的全局符号:
(1) 不允许强符号被多次定义,否则链接器报符号重复定义的错误
(2) 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,则选择强符号
(3) 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个
弱引用与强引用
对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,须被正确决议,如果没有找到该符号的定义,编译器就会报符号为定义的错误,这种被称为强引用;
与之对应还有一种弱引用,在处理弱引用时,即使该符号未被定义,链接器也不会报错,默认其为0或一个特殊的值。
GCC可以通过"__attribute__((weakref))"来声明一个外部函数的引用为弱引用。
__attribute__ ((weakref)) void fun();
int main()
{
if (NULL != fun)
{
fun();
}
}
这种弱符号和弱引用对于库来说十分有用,库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;
或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;
如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。
#include <stdio.h>
#include <math.h>
// 将math系统库函数abs声明为弱符号
int __attribute__((weak)) abs(int);
// 重新实现一个abs函数
int abs(int a)
{
return 0;
}
int main(int argc, char* argv[])
{
int s = abs((int)-5);
printf("s=%d\n", s); // s=0
return 0;
}
对于链接器来说,整个链接过程,就是将多个输入目标文件合成一个可执行二进制文件。
现代链接器,基本都是采用两步链接的方法:
(1) 空间与地址分配
扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表中。
这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。
(2) 符号解析与重定位
使用上面第一步中收集的所有信息,读取输入文件中段的数据、重定位信息(有一个重定位表Relocation Table),并且进行符号解析与重定位、调整代码中的地址(外部符号)等。
参考
《程序员的自我修养链接、装载与库》