程序的编译链接过程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序的编译链接过程相关的知识,希望对你有一定的参考价值。

还是从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)等

-----------------------------MSVC编译器-----------------------------

MSVC编译器默认使用的是__cdecl调用约定(在"C/C++" -- "Advanced" -- "Calling Convention"中设置),Windows API使用的__stdcall调用约定。

针对c语言和c++语言,MSVC有两套修饰规则:

c语言函数名修饰约定规则:(被extern "C"包裹的代码块)
1、__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为[email protected]

2、__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。

3、__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式@[email protected]

它们均不改变输出函数名中的字符大小写,这和pascal调用约定不同,pascal约定输出的函数名无任何修饰且全部大写。

c++语言函数名修饰约定规则:
1、__stdcall调用约定:
(1)以“?”标识函数名的开始,后跟函数名;
(2)函数名后面以“@@yg”标识参数表的开始,后跟参数表;
(3)参数表以代号表示:
x--void ,
d--char,
e--unsigned char,
f--short,
h--int,
i--unsigned int,
j--long,
k--unsigned long,
m--float,
n--double,
_n--bool,
....
pa--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
(4)参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
(5)参数表后以“@z”标识整个名字的结束,如果该函数无参数,则以“z”标识结束。
其格式为“[email protected]@yg*****@z”或“[email protected]@yg*xz”,例如
int test1-----“[email protected]@[email protected]
void test2-----“[email protected]@ygxxz”

2、__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@yg”变为“@@ya”。

3、__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@yg”变为“@@yi”。

:如果输出了map文件,可以在该文件中查看各函数及变量被修饰后的名称字符串。

-------------------------------------------------------------------------

函数签名(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),并且进行符号解析与重定位、调整代码中的地址(外部符号)等。

 

参考

     《程序员的自我修养链接、装载与库》 

以上是关于程序的编译链接过程的主要内容,如果未能解决你的问题,请参考以下文章

C语言中程序的编译(预处理操作)+链接详解(详细介绍程序预编译过程)

C/C++程序的编译链接过程

C语言生成可执行文件的过程——预处理编译汇编链接。学习笔记

C程序

C语言预处理 编译 汇编 链接四个阶段

C语言编译链接生成可执行文件四大步骤:预处理->编译->汇编->链接