1. 前言

学过 C 语言同学,第一个接触的例子应该就是著名的 “hello world” 程序,即便时隔多年,也能行云流水般敲出以下代码:

1
2
3
4
5
6
7
#include <stdio.h>

int main (void)
{
printf ("hello world!\n");
return 0;
}

然后使用 GCC 一气呵成地编译链接出可执行文件,并运行:

1
2
3
$ gcc main.c -o test
$ ./test
hello world!

看着屏幕输出我们想要的显示,开始难免会有一丝兴奋,原来这么简单就可以控制屏幕输出。但事实上真的有这么简单吗?#include 是做什么用的,gcc 在背后帮我们做了什么事情,./test 开始运行时背后又有什么故事。这就是本文的重点:编译、链接、装载。

实际上,一个 c 文件到最后运行,一般会经历以下过程,背后的故事,很长:

img

2. 预处理

预处理是将头文件进行展开,并进行相关的宏替换和处理,删除注释等,最后生成 .i 文件,使用命令 gcc -E hello.c -o hello.i 可 以生成预处理文件 ,其后缀为.i.i 文件是一个可读文本,并不包含任何宏定义,预处理过程主要处理源代码内以 “#” 开始的编译指令,如 “#include” , “#define”,它的主要处理规则如下:

  • 将所有 #define 删除,并展开所有宏定义。
  • 处理所有条件编译指令,如 #if#ifdef#elif 等。
  • 处理 #include 预编译指令,将被包含的文件插入到预编译指令的位置,这个过程是递归的,被包含的文件可能也包含其他文件。
  • 删除所有注释。
  • 添加行号和文件名标识,用于编译时编译器产生调试信息。
  • 保留所有的 #pragma 编译器指令,因为编译器要用到他们。

预处理阶段完成后,旧进入了程序的编译阶段。

3. 编译

源代码是无法直接运行的,这是因为 CPU 能直接解析并运行的不是源代码而是本地代码程序,对于 CPU 来讲本地代码就是转换成机器语言的程序,而这种转换的程序称为编译器。编译器是和 CPU 类型相关的,因为编译器本身也是程序,所以也需要运行环境,比如有 windos 用的 c 编译器,还有 Linux 用的 c 编译器,此外还有一种交叉编译器,它生成的是和运行环境中的 CPU 不同的 CPU 所使用的本地代码,嵌入式开发一般使用的就是交叉编译器。(为什么要有交叉编译?原因在于程序的编译过程中会占用很大的内存和磁盘空间,且对 CPU 处理速度要求较高,而目标平台,如路由器根本不可能达到要求,所以一般进行嵌入式开发时,是先在 PC 主机上编译出目标代码,然后下载到板子上跑起来运行)

编译就是把完成预处理的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件,这个过程是程序构建的核心部分。使用命令 gcc –S hello.i –s hello.s 就将.i 文件翻译成汇编代码,并输出为.s 文件,此文件可读,由汇编代码和一些伪代码组成。

编译过程一般可以分为 6 步:扫描,语法分析,语义分析,源代码优化,代码生成和目标代码优化,整个过程见下图:

img

3.1 编译过程

以如下 C 代码为例 分析编译阶段的每一个过程。

1
Array [index] = (index + 4) * (2 + 6)

3.1.1 词法分析

首先源代码程序被输入到扫描器,它进行词法分析,将源代码的字符序列分割成一系列的记号,如下:

记号 类型
array 标识符
[ 左方括号
index 标识符
] 右方括号
= 赋值
( 左圆括号
index 标识符
+ 加号
4 数字
) 右圆括号
* 乘号
( 左圆括号
2 数字
+ 加号
6 数字
) 右圆括号

3.1.2 语法分析

然后语法分析器将对扫描器产生的记号进行语法分析,从而产生语法树,如下图所示:

img

从中可以看到整个语句被看作是一个赋值表达式,赋值表达式的左边是一个数组表达式,右边是一个乘法表达式,数组表达式又由两个符号表达式组成等。符号和数字是最小的表达式,他们不是由其他表达式来组成的,所以他们通常作为整个语法树的叶节点。

在语法分析过程的同时,很多运算符号的优先级和含义也被确定下来了,比如乘法表达式的优先级比加法高,而圆括号表达式的优先级比乘法高等等。如果出现表达式不合法,比如各种括号不匹配,表达式中缺少操作符等,编译器就会报告语法分析的错误。

3.1.3 语义分析

接下来进行语义分析,语法分析并不了解这个语句是否真正有意义,所以编译器要进行语义分析,这个过程由语义分析器完成。

编译器能够进行的只有静态语义分析,即可以在编译期间确定的语义,静态语义通常包含声明和类型的匹配,类型的转换,比如在将一个浮点型赋值给一个指针的时候,语义分析程序会发现类型不匹配,编译器将会报错。相对应的,动态语义分析一般指的是运行起出现的语义相关问题,比如将 0 作为除数是一个运行时的语义错误。

经过语义分析阶段后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序将会在语法数种插入相应的转换节点,如下图的语义树:

img

可以看到语义分析还对符号表里的符号类型做了更新。

3.1.4 中间代码生成

现代编译器有着多层次的优化,往往在源代码级别会有一个优化过程,这个给过程是由源码级优化器。源码级优化器会在源代码级别进行优化,比如上述表达式中的(2 + 6)就可以被优化掉,因为它的值在编译期就可以确定,经过优化后的语法树如下为优化后的语法树:

img

可以看到 (2 + 6)的表达式被优化为 8。其实直接在语法树上进行优化比较困难,所以源代码优化器往往将整个语法树转换为中间代码,它是语法树的顺序表示,已经非常接近目标代码了。但是它一般跟目标机器和运行时环境是无关的,比如它不包含数据的尺寸,变量地址和寄存器的名字等,中间代码由很多形式,比较常见的有三地址码,我们上面的语法树被翻译为三地址码后的形式如下:

1
2
3
4
t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array [index] = t3

3.1.5 目标代码的生成和优化

代码生成器将中间代码转换为目标机器代码,这个过程依赖于目标机器,因为不同的机器拥有不同的字长,寄存器,整数数据类型等。对于上面的例子中的中间代码,代码生成器可能会生成如下代码序列,见下图目标机器(x86)代码:

img

最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式,使用位移来代替乘法运算,删除多余指令等。在上面的例子中,乘法由一条相对复杂的基址比例变址寻址的 lea 完成,随后由一条 mov 指令完成赋值操作,这条 mov 指令的寻址方式于 lea 是一样的,见下面的优化代码:

img

经过以上步骤,最终生成了目标代码,为编译阶段的产物(一种 ELF 文件,ELF 文件的介绍会在第 5 章展开描述)。但是目标代码有个问题,index 和 array 的地址还没有确定,这其实是链接要做的事情,下面会继续分析。

4. 汇编

编译器将预处理文件转换为特定机器语言,但是对于 CPU 来讲,能处理的是自己的指令,所以还需要汇编器将汇编代码转换为特定机器指令。汇编器的作用是将汇编代码转变为机器可以执行的指令,每一个汇编语句几乎否对应一条机器指令。所以汇编器相对于编译器来讲比较简单,没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表翻译即可。

汇编器将汇编文件转换为可重定位目标文件(使用命令 gcc –c hello.s –o hell.o 或 或 as hello.s –o hello.o),即 .o 文件,是一个不可读的二进制文件(是一种 ELF 文件),此文件可以使用 objdumpreadelf 打开。这个过程会根据.s 文件中由编译器生成的符号构造一张符号表,内部包含符号表条目,用以链接使用,除此之外还会生成各个节。

5. 链接

5.1 链接的背景

在现代软件开发过程中,软件规模往往很大,如果都放在一个模块肯定无法想象,所以大型软件一般会拥有多个模块,这些模块之间相互依赖又相对独立。按照这种层次化和模块化存储组织源代码有很多好处,比如代码可读性较高,每个模块可单独开发,编译,测试,改变部分代码不需要编译整个程序等。

在一个程序被分割为多个模块后,这些模块如何组织形成一个单一的程序是必须解决的问题,模块之间如何组合的问题归结为模块之间如何通信的问题,最常见的是属于静态预言的 C/C++ 模块之间有两种通信方式,分别是函数调用和变量访问。函数访问必须知道目标函数的地址,变量访问也一样,所以这两种方式都可以归结为模块间的符号引用。

而链接的目的就是将模块组合成单一程序,并赋予这些符号地址,实现模块之间的通信,链接的最终产物是一个可执行文件(也是一种 ELF 文件)

5.2 ELF 文件(Executable Linkable Format)

上面提到汇编和链接过程的产物是一种 ELF 文件,那么下面介绍一下 ELF 文件。

ELF 是一种文件格式,是 Linux 上默认的目标文件格式,ELF 文件可以分为以下几种,分别是:

  • 可重定位目标文件:包含了代码段和数据,可与其他 ELF 文件进行合并,创建一个可执行目标文件或共享目标文件,如 Linux 下的.o 文件,静态库也是这一类文件。
  • 可执行目标文件:包含二进制代码和数据,可以被加载器加载执行,链接的最终产物就是一个可执行目标文件。
  • 共享目标文件 ( .so),在链接过程和运行过程两个阶段都需要被使用到:链接过程中,编译器将其他 Relocatable Object File 与存在调用关系的 Shared Object File 进行链接处理后,输出 Executable File;运行过程中,动态链接器处理加载后的 Executable file 查找其依赖的 Shared Object File,进行加载处理,创建整个进程的运行实例。
  • 核心转储文件(core 文件),当程序崩溃时,会在核心文件中记录整个进程的镜像信息。

ELF 文件格式提供了两种视图,分别是链接视图和执行视图:

img

链接视图是以节(section)为单位,执行视图是以段(segment)为单位。接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。

目标文件.o 里的代码段 .text 是 section(汇编中.text 同理),当多个可重定向文件最终要整合成一个可执行的文件的时候(链接过程),链接器把目标文件中相同的 section 整合成一个 segment,在程序运行的时候,方便加载器的加载。

ELF 文件主要包含三个 header,描述文件格式构成。

5.2.1 ELF Header

使用命令 readelf -h 可以查看一个 ELF 文件的 ELF Header 信息。

img

比较重要的成员有:e_ident(ELF 文件幻数)、e_machine(比如可执行文件 ET_EXEC)、e_entry(程序入口虚拟地址)等等。

ELF 头部定义如以下结构体所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* /include/uapi/linux/elf.h */
typedef struct elf32_hdr
{
unsigned char e_ident [EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

简单归纳各字段

elf header:
    magic num、version、arch、endian、flag、elf header size
    elf type: EXEC (Executable file)、REL (Relocatable file)、DYN (Shared object file)
    entry point: EXEC 文件才有,程序入口虚拟地址
    program hdr offset/size/num in file:
    section hdr offset/size/num in file:
    str table of section hdr idx:

5.2.2 Section Header

使用命令 readelf -S 可以查看一个 ELF 文件的 Section Header 信息。

![img](Section Header实例png.png)

一个 ELF 文件中到底有哪些具体的 sections,由包含在这个 ELF 文件中的 section head table (SHT) 决定。每个 section 描述了这个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。

下面介绍下常见和比较重要的 section:

sh_name sh_type description
.text SHT_PROGBITS 代码段,包含程序的可执行指令
.data SHT_PROGBITS 包含初始化了的数据,将出现在程序的内存映像中
.bss SHT_NOBITS 未初始化数据,因为只有符号所以
.rodata SHT_PROGBITS 包含只读数据
.comment SHT_PROGBITS 包含版本控制信息
.eh_frame SHT_PROGBITS 它生成描述如何 unwind 堆栈的表
.debug SHT_PROGBITS 此节区包含用于符号调试的信息
.dynsym SHT_DYNSYM 此节区包含了动态链接符号表
.shstrtab SHT_STRTAB 存放 section 名,字符串表。Section Header String Table
.strtab SHT_STRTAB 字符串表
.symtab SHT_SYMTAB 符号表
.got SHT_PROGBITS 全局偏移表
.plt SHT_PROGBITS 过程链接表
.relname SHT_REL 包含了重定位信息,例如 .text 节区的重定位节区名字将是:.rel.text

Section 头部定义如以下结构体所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* /include/uapi/linux/elf.h */
typedef struct elf32_shdr {
Elf32_Word sh_name; // 节区名,名字是一个 NULL 结尾的字符串。
Elf32_Word sh_type; // 为节区类型
Elf32_Word sh_flags; // 节区标志
Elf32_Addr sh_addr; // 节区的第一个字节应处的位置。否则,此字段为 0。
Elf32_Off sh_offset; // 此成员的取值给出节区的第一个字节与文件头之间的偏移。
Elf32_Word sh_size; // 此成员给出节区的长度(字节数)。
Elf32_Word sh_link; // 此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
Elf32_Word sh_info; // 此成员给出附加信息,其解释依赖于节区类型。
Elf32_Word sh_addralign; // 某些节区带有地址对齐约束.
Elf32_Word sh_entsize; // 给出每个表项的长度字节数。
} Elf32_Shdr;

简单归纳各字段

section header: (用于 link 的 elf 必须有,其他文件不是必要的)
    name(string tbl index)、offset、size、addr
    type:
        PROGBITS
        REL:重定位,如.rel.text
        NOBITS:
        STRTAB:字符串表,格式为 str1 \0 str2 \0 ... (其他使用该 str 时不需要记录 size)
        SYMTAB:格式
        
    flag: write、alloc、execute、merge、strings、info、exclude、group
    
    .rel.text:
         Offset     Info    Type            Sym.Value  Sym. Name
        00000038  00000e04 R_MIPS_26         00000000   b_func_1

    .symtab:
    val:取决 type,可能是地址(相对所在 section 的 offset)
    type 有 SECTION  OBJECT(变量) FUNC  NOTYPE(外部 sym) FILE(文件名 a.c)
    Ndx: ABS (文件名)  UND(外部 sym)其他为所在 section index
    Bind:LOCAL   GLOBAL(外部可见)
    例子:
        Num:    Value  Size Type    Bind   Vis      Ndx Name
        13: 00000000   200 FUNC    GLOBAL DEFAULT    1 a_func_1
        14: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND b_func_1

5.3.3 Program Header

使用命令 readelf -l 可以查看一个 ELF 文件的 Program Header 信息。

img

程序头是一个结构的数组,每一个结构都表示一个段 (segments)。在可执行文件或者共享链接库中所有的节 (sections) 都被分为不同的几个段 (segments)。

程序头的索引地址 (e_phoff)、段数量 (e_phnum)、表项大小 (e_phentsize) 都是通过 ELF 头部信息获取的。

Program 头部定义如以下结构体所示:

1
2
3
4
5
6
7
8
9
10
11
/* /include/uapi/linux/elf.h */
typedef struct elf32_phdr {
Elf32_Word p_type; /* Magic number and other info */
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;

简单归纳各字段

Program Headers:(用于进程 img 加载,其他文件不是必要的)
    例子:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
      REGINFO        0x000094 0x00400094 0x00400094 0x00018 0x00018 R   0x4
      LOAD           0x000000 0x00400000 0x00400000 0x003a0 0x003a0 R E 0x10000
      LOAD           0x0003a0 0x004103a0 0x004103a0 0x00020 0x00050 RW  0x10000

     Section to Segment mapping:
      Segment Sections...
       00     .reginfo
       01     .reginfo .text
       02     .data .sbss .bss

从加载的角度来看,ELF 文件被分成了许多段,ELF 文件中的代码,链接信息和注释都以段的形式存访。每个段在程序表头表中有一个描述项,分别包含段的类型,段的驻留位置相对于 ELF 文件开始处的偏移量,段在内存中的首地址,段的物理地址,段在文件中的大小,段在内存中的大小,段的对齐标志,如上图所示。

一个可执行文件至少要有一个可加载类型的段,这种类型的段会被装载或映射进内存中,这个会在后面分析程序加载流程时会描述到。

5.3 符号和符号表

5.3.1 符号

符号就是程序中的函数和变量,函数名或变量名就是符号名。每个 ELF 文件都有一个符号表,它包含了在此文件中定义和引用的符号,符号类型可以分为以下三类:

l Global symbols 即全局符号,是由当前文件定义并能被其他模块引用的符号;

l External symbols 即外部定义的全局符号,是由其他模块定义并被当前文件所引用的全局符号;

l Local symbols 即本模块的局部符号,仅由本模块定义的带有 static 的 C 函数和全局变量。

在编译器的代码里面。用下面结构体来描述一个符号,见下图

img

5.3.2 符号表

ELF 文件中的.symtab 节记录着符号表的信息,用 readelf –s hello.o 可以看到符号表的内容,当前 hello.o 内的符号表信息如下图:

img

从上面的符号表信息中可以看到,当前有 10 个符号,其中符号 main 是 hello.o 内第 1 节(.text)偏移量为 0 的一个全局符号,占 21 字节,type 类型为 Func,其他的以此类推,上图中,ABS 表示不该被重定位,UND 表示未定义的含义。hello.o 中的节的信息如下:

img

5.3.3 C++ 符号问题

C++ 允许不同参数类型的函数拥有一样的函数名,这就是所谓的重载,那么 C++ 是如何来处理函数重载这一情况的呢?

答案是函数签名,函数签名包含了一个函数的信息,包括函数名,参数类型和名称空间及其他信息。函数签名用于识别不同的函数。在编译器及链接器处理符号时,他们使用某种名称修饰的方法,使得每个函数对应一个修饰后名称,以此来使得编译器和链接器都认为这些重载函数是不同的函数。

以 GCC 的修饰方法为例,当 GCC 按照一定的规则(不具体展开,不同编译器对名称的修饰方法可能不同)完成对 C++ 名称的修饰之后,就形成了对应的修饰名称。举例如下,下面是一段 C++ 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int func (int a)
{
cout << a << endl;
}

float func (float a)
{
cout << a <<endl;
}

int main ()
{
int a = 3;
float b = 3.14;

func (a);
func (b);

return 0;
}

编译后查看符号表可以得到:

1
2
3
4
5
$ g++ test.cpp -g -o test
$ nm test | grep func
0000000000400945 t _GLOBAL__sub_I__Z4funci
0000000000400896 T _Z4funcf
000000000040086d T _Z4funci

可以看到被修饰后的符号名称为 _Z4funcf 和 _Z4funci ,使用 c++filter 就可以解析对 func 修饰后的名称,解析结果为 func (int) ,如下:

1
2
3
4
$ c++filt  _Z4funcf      
func (float)
$ c++filt _Z4funci
func (int)

5.4 链接脚本

每一个链接过程都由链接脚本 (linker script,一般以 lds 作为文件的后缀名) 控制。 链接脚本主要用于规定如何把输入文件内的 section 放入输出文件内,并控制输出文件内容各部分在程序地址空间内布局。链接器有个默认的内置链接脚本,可以使用 ld -verbose 查看。ld 链接选项 -r-N 可以影响默认的链接脚本,-T 选项用以指定特定的链接脚本,它将代替默认的链接脚本。也可以使用暗含的链接脚本以增加自定义的链接命令。

5.4.1 链接脚本举例分析

内核镜像的第一个名称为 vmlinux ,vmlinux 是通过源码编译,汇编,链接而成的 ELF 文件,因此这个文件包含了 ELF 文件应有的属性及各种调试信息(这个阶段的 vmlinux 特别大,不能直接在目标机器上运行,因此要进一步压缩),内核目录 /arch/arm/kerner/vmlinux.lds.S 会在编译阶段根据宏定义和传入的参数构建出针对特定平台和架构的 vmlinux.lds 链接脚本,由此链接脚本来指导 vmlinux 的生成,下面我们分段来分析此 vmlinux.lds.S 的内容。

1
2
OUTPUT_ARCH (arm)
ENTRY (stext)

这段脚本的含义是指定链输出文件的指令架构为 ARM,且入口函数为 stext,此函数可以在符号表内找到。在这里 OUTPUT_ARCH 用来指定输出架构,使用 objdump –f vmlinux 可以看出目标文件的体系架构;而 ENTRY 用来设置入口点,其参数为符号名称,有几种设置入口点的方法,如下:

img

链接器会尝试上述几种方法来设置入口点,直到成功。

接下来进入 SECTIONS 命令,它告诉链接器如何将输入段映射到输出段,以及将如何将输出段放到内存中,其命令格式为:

img

每个 sections-command 可能为:ENTRY 命令,符号赋值,输出段的描述等。一个输出段的完整描述如下图:

img

其中,大部分属性在输出段中并不需要,我们会在下面的分析中看到,其中 section 后面必须跟一个空格,这样段名就没有歧义,另外,冒号和花括号是必须的。

img

下面开始定义目标文件中的各个 section,首先对 “.” 进行赋值,它叫做位置计数器,对应内存里的一个虚拟地址,若不显式的为其赋值,则在每增加一个 section 后对其会自增。(可以看到当前内核的起始地址为 PAGE_OFFSET,它的值由内核宏定义控制,而 TEXT_OFFSET 为内核镜像加载的偏移量,中间预留的一部分内存用来做页表使用。)对 “.” 赋值后,开始对此位置的 section 进行描述。

第一个 section 为 .head.text ,在此 section 中对 _text 进行赋值,其中 HEAD_TEXT 为宏定义,它展开为 *(.head.text) ,即将所有的目标文件中的 .head.text 加载到此段。

img

接下来加载 .text 段,用 _stext _etext 两个变量来记录 .text 的起始位置和结束位置, 他们可以在代码内进行访问,ARM_TEXT 为宏定义,展开后指向所有目标文件中与代码段相关的段,其中 ALIGN 的作用是将地址按指定字节进行对齐。

img

接下来加载 _ex_tables 段,其加载地址是按 4 字节进行对齐,用变量 __start_ex_table__stop_ex_table来记录其齐时地址和结束地址,这块可以看到有个 unwind 段,此段是用来进行栈回溯的。

img

然后加载 init 段,从 __init_begin 开始到 __init_end 结束,在 Linux 初始化完成后,这个段内的内存会被清空释放,只需要在初始化时使用以此,没有必要再驻留再内存中。

img

进一步加载.data 段,即已经初始化的内核数据段,用 _sdata 和 _edata 来记录 .data 段的起始地址和结束地址。

img

加载 bss 段,即未初始化的内核数据段,.end 记录加载结束位置。

上面的汇编脚本经过编译后生成真正的链接脚本 vmlinux.lds ,由其指导目标文件的链接,此链接脚本就是针对于特定平台而生成的,我们看一下最终生成 vmlinux 文件的各个段的排列如下图所示:

img

可以看到目标文件的布局是严格按照链接脚本生成的。

5.5 链接的过程

上面讲过链接的过程就是把各个模块之间相互引用部分处理好,使得各个模块之间能够正确的衔接,最终形成可执行目标文件。链接的基本过程包含了两个步骤,分别是空间与地址分配,符号解析和重定位。

5.5.1 空间与地址分配

空间与地址分配的主要过程是扫描所有的输入目标文件,获得他们的各个段的长度,属性和位置,将输入目标文件中的符号表与所有的符号定义和符号引用收集起来,统一放到一个全局符号表,计算出各个段合并后的长度和位置,建立映射关系。

经过这一步后,每个符号都有了对应的虚拟地址。

5.5.2 符号解析

符号解析就是将符号的引用和符号的定义建立关联,这个过程中链接器会查找所有输入目标文件的符号表组成的全局符号表,查找函数或变量是否有对应的定义,如果没有则抛出错误终止链接。具体是怎么做的呢?首先将定义三个集合 E(可重定位目标文件集合), U(未解析符号集合), D(已定义符号集合),然后对于每一个输入的目标文件,将其添加进 E 中,并更新 U 和 D 反映当前文件的符号定义和引用情况,不断重复此过程,直到 U 和 D 不再发生变化,如果链接器扫描完所有输入文件后,发现 U 是非空的,那么链接器会输出一个错误并终止,否则进行对 E 中目标文件进行合并,构建可执行目标文件。

5.5.3 重定位

重定位就是在符号解析过程中将可执行文件中的符号引用处的地址修改为重定位后的地址。比如在 hello.o 文件内查看 main 的符号定义,其虚拟地址为 0,如下:

img

而在 hello 中可以看到 main 的地址见下

img

重定位完成后,最后生成的可执行目标文件中就不需要可重定位的条目,使用 readelf -r hello 可以看到,另外可以使用 objdum –h 命令,查看重定位后的各段的虚拟地址,如在 hello.o 中 ,.data 的地址为 0,因为还没有进行重定位,无法加载进内存运行,重定位后的结果见下图

img

根据链接发生时机,将链接分为静态链接和动态链接,下面分别描述。

5.6 静态链接

静态链接的核心内容就是将多个目标文件的相同段合并形成一个可执行文件,以 a.c 和 b.c 文件的链接过程为例,是在程序被加载进内存前。下面以一个例子来分析静态链接的过程。

5.6.1 静态链接过程

给出两个文件,a.c 如下:

1
2
3
4
5
6
7
8
extern int shared;

int main (void)
{
int a = 100;
swap (&a, &shared);
return 0;
}

b.c 如下:

1
2
3
4
5
6
7
8
int shared = 1;

void swap (int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}

a.c 种引用了 b.c 文件种的 share 变量和函数,对上述两个使用命令 gcc -c a.c b.c 编译生成 a.o 和 b.o ,查看 a.o 内的符号表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ gcc -c a.c b.c 
$ readelf -s a.o

Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 44 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap

可以看到 a.o 中引用的两个符号 shared、swap 是未定义的(UND),下面看一下 b.o 的符号表,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ readelf -s b.o

Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 shared
9: 0000000000000000 44 FUNC GLOBAL DEFAULT 1 swap

可以看到变量和函数名是存在的,不是未定义的,下面使用命令 ld a.o b.o -e main -o ab ,把两个目标文件合成一个可执行文件,其中 –e 是指定程序入口是 main 函数,-o 是表示链接后的文件名,看一下 ab 的符号表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ld a.o b.o -e main -o ab  
$ readelf -s ab

Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004000e8 0 SECTION LOCAL DEFAULT 1
2: 0000000000400140 0 SECTION LOCAL DEFAULT 2
3: 0000000000600198 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
6: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
7: 0000000000000000 0 FILE LOCAL DEFAULT ABS
8: 0000000000400114 44 FUNC GLOBAL DEFAULT 1 swap
9: 0000000000600198 4 OBJECT GLOBAL DEFAULT 3 shared
10: 000000000060019c 0 NOTYPE GLOBAL DEFAULT 3 __bss_start
11: 00000000004000e8 44 FUNC GLOBAL DEFAULT 1 main
12: 000000000060019c 0 NOTYPE GLOBAL DEFAULT 3 _edata
13: 00000000006001a0 0 NOTYPE GLOBAL DEFAULT 3 _end

可以看到 main,share, swap 三个符号都有,并且不再是 UND。上面提到静态链接规则其实是将目标文件的相似段进行合并,那么看一下是是如何进行合并的,以 text 段为例,a.o 的 text 段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ objdump -h a.o

a.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000002c 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 0000006c 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 0000006c 2**0
ALLOC
3 .comment 0000002c 0000000000000000 0000000000000000 0000006c 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000098 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

可以看到该 text 段的大小为 2c,再来看一下 b.o 的 text 段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ objdump -h b.o

b.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000002c 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 0000006c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000070 2**0
ALLOC
3 .comment 0000002c 0000000000000000 0000000000000000 00000070 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000009c 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000a0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

b.o 的 text 段大小为 2c,看一下链接形成的可执行文件 ab 的段信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ objdump -h ab

ab: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000058 00000000004000e8 00000000004000e8 000000e8 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000058 0000000000400140 0000000000400140 00000140 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 0000000000600198 0000000000600198 00000198 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .comment 0000002b 0000000000000000 0000000000000000 0000019c 2**0
CONTENTS, READONLY

2c + 27 = 53,由此证明了链接的规则,同时在链接完成后,各个段的虚拟地址都已经被计算好了,为文件执行做准备,同时做一个简单计:在 ab 中,text 段的虚拟地址为 4000e8 ,因为 swap 在 b.o 中的偏移量为 0,所以进行相似段合并时,swap 的地址为 4000e8 + 27 = 40010f,查看 swap 的地址如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ readelf -s ab

Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004000e8 0 SECTION LOCAL DEFAULT 1
2: 0000000000400140 0 SECTION LOCAL DEFAULT 2
3: 0000000000600198 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
6: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
7: 0000000000000000 0 FILE LOCAL DEFAULT ABS
8: 0000000000400114 44 FUNC GLOBAL DEFAULT 1 swap
9: 0000000000600198 4 OBJECT GLOBAL DEFAULT 3 shared
10: 000000000060019c 0 NOTYPE GLOBAL DEFAULT 3 __bss_start
11: 00000000004000e8 44 FUNC GLOBAL DEFAULT 1 main
12: 000000000060019c 0 NOTYPE GLOBAL DEFAULT 3 _edata
13: 00000000006001a0 0 NOTYPE GLOBAL DEFAULT 3 _end

与预期计算相符,其他符号的地址都是这么计算出来的,链接后,每个符号的虚拟地址已经确定,那么在确定符号的虚拟地址后,链接器根据符号的地址对每个需要重定位的指令进行修正,用 objdump 反汇编编译出的指令,ab 的汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ objdump -d ab

ab: file format elf64-x86-64


Disassembly of section .text:

00000000004000e8 <main>:
4000e8: 55 push % rbp
4000e9: 48 89 e5 mov % rsp,% rbp
4000ec: 48 83 ec 10 sub $0x10,% rsp
4000f0: c7 45 fc 64 00 00 00 movl $0x64,-0x4 (% rbp)
4000f7: 48 8d 45 fc lea -0x4 (% rbp),% rax
4000fb: be 98 01 60 00 mov $0x600198,% esi
400100: 48 89 c7 mov % rax,% rdi
400103: b8 00 00 00 00 mov $0x0,% eax
400108: e8 07 00 00 00 callq 400114 <swap>
40010d: b8 00 00 00 00 mov $0x0,% eax
400112: c9 leaveq
400113: c3 retq

0000000000400114 <swap>:
400114: 55 push % rbp
400115: 48 89 e5 mov % rsp,% rbp
400118: 48 89 7d e8 mov % rdi,-0x18 (% rbp)
40011c: 48 89 75 e0 mov % rsi,-0x20 (% rbp)
400120: 48 8b 45 e8 mov -0x18 (% rbp),% rax
400124: 8b 00 mov (% rax),% eax
400126: 89 45 fc mov % eax,-0x4 (% rbp)
400129: 48 8b 45 e0 mov -0x20 (% rbp),% rax
40012d: 8b 10 mov (% rax),% edx
40012f: 48 8b 45 e8 mov -0x18 (% rbp),% rax
400133: 89 10 mov % edx,(% rax)
400135: 48 8b 45 e0 mov -0x20 (% rbp),% rax
400139: 8b 55 fc mov -0x4 (% rbp),% edx
40013c: 89 10 mov % edx,(% rax)
40013e: 5d pop % rbp
40013f: c3 retq

这就是重定位,还有一个问题是链接器是怎么知道哪些指令要进行重定位呢?在 ELF 文件内有一个重定位表的结构专门用来保存这些与重定位相关的信息,使用 readelf –r a.o 可以看到目标文件的重定位表,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ objdump -r a.o

a.o: file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000014 R_X86_64_32 shared
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text

其中 “0000000000000021 R_X86_64_PC32 swap-0x0000000000000004“ 表示 swap 在 text 段内被引用了,需要进行重定位,重定位地址为 text 段偏移量为 21 字节处,看一下 a.o 的反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ objdump  -D a.o

a.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push % rbp
1: 48 89 e5 mov % rsp,% rbp
4: 48 83 ec 10 sub $0x10,% rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4 (% rbp)
f: 48 8d 45 fc lea -0x4 (% rbp),% rax
13: be 00 00 00 00 mov $0x0,% esi
18: 48 89 c7 mov % rax,% rdi
1b: b8 00 00 00 00 mov $0x0,% eax
20: e8 00 00 00 00 callq 25 <main+0x25>
25: b8 00 00 00 00 mov $0x0,% eax
2a: c9 leaveq
2b: c3 retq

偏移的 21 字节刚好是 e8 (call 指令)后的位置,这里的 21 也叫做重定位入口。(在重定位段中,swap 的 TYPE 属性是 R_X86_64_PC32,这个表示的是,不要在重定位入口处直接填写 swap 的虚拟内存地址,而要填 swap 相对 call 指令下一条指令的偏移量。R_X86_64_PC32 这里也叫做重定位入口类型。

如果我们在链接指令 ld a.o b.o –e main –o ab 中,不链接 b.o ,则会出现如下错误,这是静态链接常见的一种编译报错,即符号未定义引起的报错。

1
2
3
4
$ ld a.o -e main -o ab
a.o: In function `main':
a.c:(.text+0x14): undefined reference to `shared'
a.c:(.text+0x21): undefined reference to `swap

5.7 动态链接

5.7.1 动态链接的背景

上面分析了静态链接的过程,可以看到静态链接有一个明显的特点就是必须是在程序运行前完成。这样会存在以下两个缺点:

  • 版本更新麻烦。静态库和所有的软件一样,需要定期维护和更新。如 lib 更新了,还需要重新编译可执行文件,尽管可能是一个很小的改动,却导致整个程序需要重新下载,全量更新。
  • 内存空间浪费。几乎每个 C 程序都使用标准 I/O 函数,比如 printf 和 scanf 在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行 50-100 个进程的典型系统上,这会是对稀少的存储器系统资源的极大浪费。

为了解决以上问题,引入了共享库和动态链接。共享库是致力于解决静态库缺陷的一个现代创新产物,它是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来,这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。

在任何给定的文件系统中,对于一个库只有一个共享库文件。 所有引用该库的可执行目标文件共享这个共享库文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌入到引用它们的可执行的文件中;在存储器中,一个共享库的.text 节只有一个副本可以被不同的正在运行的进程共享。

这样一来,既节省了磁盘空间和内存空间,又使得程序的升级更容易进行,不用再重新编译整个代码,只需要对原有旧目标进行替换即可。

5.7.2 动态链接原理分析

动态链接的基本步骤分为以下几步:

  • 动态链接器的自举(动态链接器即 ld.so 本身也是一个共享对象,但是它不依赖于任何共享对象,如下图所示,其次动态链接器本身所需的全局变量和静态变量的重定位由其自己完成,这就是动态链接器的自举,就像内核的自我解压)

动态链接的基本步骤分为一下几步:

(1)动态链接器的自举(动态链接器即 ld.so 本身也是一个共享对象,但是它不依赖于任何共享对象,如下图所示,其次动态链接器本身所需的全局变量和静态变量的重定位由其自己完成,这就是动态链接器的自举,就像内核的自我解压)

1
2
$ ldd /lib/x86_64-linux-gnu/ld-2.19.so 
statically linked
  • 动态链接装载共享对象,即动态链接器将可执行文件依赖的代码段和数据段映射到进程的地址空间中,直到所有的依赖对象都被装载成功。
  • 符号的重定位。

完成以上步骤后,动态链接器将控制权移交给进程,进程开始运行。

我们重点分析动态链接时,其符号是如何被重定位的,介绍 GOT,PLT 等相关内容,同时举例分析重定位是如何一步一步实现的。

5.7.3 地址无关代码(Position-independent Code)

当动态链接模块被装载进内存空间后,指令部分在多个进程间共享,若使用重新装载的方法,需要修改指令(像静态链接一样),没有办法做到同一份指令被多个进程共享,因为重定位后的指令对于每个进程来讲是不一样的,它不像动态链接库中的可修改数据部分,对每个进程来讲都有一份拷贝,可以使用装载时重定位的方法。

那么如何处理这种动态库的指令部分不能共享的问题呢?我们希望的是程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现方法就是把指令中需要修改的部分分离出来,跟数据部分放在一块,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这就是地址无关代码,这就保证了二进制文件不论被加载到哪个位置,都可以正确执行。

我们将共享对象模块种的地址引用按照是否跨模块分为两类,模块内部引用和模块外部引用,按照不同的引用方式又可以分为指令引用和数据访问,同时给出其地址引用方式

各种地址引用方式
指令跳转,引用 数据访问
模块内部 相对跳转和调用 相对地址访问
模块外部 间接跳转和调用(GOT) 间接访问(GOT)

其中模块内部的指令引用和数据访问都可以使用相对偏移来进行计算,因为模块内部之间他们的相对偏移是固定的,而模块外部的函数调用需要使用 GOT 进行间接跳转。

5.7.4 GOT(GLOBAL Offset Table)

在进行动态链接时,程序的代码段是只读属性,在重定位过程中是不能被修改的,因为多个进程是共享动态库中的指令部分,但是每个进程却拥有动态共享库的数据部分,那就只能通过修改数据部分来完成重定位过程。

GOT 即全局偏移表,它是 ELF 在其数据段内建立一个指向被引用的外部变量的指针数组,保存了所有外部符号的地址信息,GOT 被保存在数据段,所以可以在装载时被修改,并且每个进程都有独立的副本,互不影响。假如在执行的指令中,需要引用符号 A,但是 A 存在于动态库中,链接过程并不知道它的地址,于是将 A 的地址部分改写为 GOT 表中的一项,在编译阶段 GOT 表中没有真实数据的,但是在动态链接阶段,动态链接器可以将符号 A 的真实地址写到 GOT 表中对应的数据项,这样就产生流对指令 A 的正确引用,GOT 表中的每一个表项表示的是运行时的符号的真实地址。

这就是引入 GOT 表的原因。

5.7.5 PLT(Procedure Linkage Table)

相对于静态链接而言,动态链接比较灵活,当然也带来了性能上的一些损失,ELF 程序在静态链接下要比动态链接要快,大约 1%~5%,当然这也取决于程序的运行环境。但如果在动态链接的时候,没有被用到的函数也被重定位,则会延长链接时间,所以 ELF 采用了一种延迟绑定(Lazy Binding)策略,基本思想就是在函数第一次被用到时才进行绑定,如果没有用到则不被绑定。所以程序开始执行时,模块间的函数都没有被绑定,而是需要时才由动态链接器来负责绑定,这种做法可以加快程序启动速度。

而延迟绑定的实现是由 PLT 表来实现,ELF 文件中,PLT 表和 GOT 表几乎是同时存在,因为运行时不能修改指令,所以通过数据部分的 GOT 表来传递运行时符号的真实地址,而 PLT 是一小段跳转指令,来实现函数的间接调用。

下面举例来分析一下 PLT 和 GOT 是如何配合来实现函数的间接调用的。

5.7.6 举例分析

回到我们最初的 hello world 程序,其虚拟空间地址空间内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat /proc/15457/maps
00400000-00401000 r-xp 00000000 08:13 2883925 /home/yw/mywork/my_programe/test
00600000-00601000 r--p 00000000 08:13 2883925 /home/yw/mywork/my_programe/test
00601000-00602000 rw-p 00001000 08:13 2883925 /home/yw/mywork/my_programe/test
7fccada35000-7fccadbf3000 r-xp 00000000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7fccadbf3000-7fccaddf3000 ---p 001be000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7fccaddf3000-7fccaddf7000 r--p 001be000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7fccaddf7000-7fccaddf9000 rw-p 001c2000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7fccaddf9000-7fccaddfe000 rw-p 00000000 00:00 0
7fccaddfe000-7fccade21000 r-xp 00000000 08:13 1310820 /lib/x86_64-linux-gnu/ld-2.19.so
7fccae005000-7fccae008000 rw-p 00000000 00:00 0
7fccae01f000-7fccae020000 rw-p 00000000 00:00 0
7fccae020000-7fccae021000 r--p 00022000 08:13 1310820 /lib/x86_64-linux-gnu/ld-2.19.so
7fccae021000-7fccae022000 rw-p 00023000 08:13 1310820 /lib/x86_64-linux-gnu/ld-2.19.so
7fccae022000-7fccae023000 rw-p 00000000 00:00 0
7ffefa50c000-7ffefa52d000 rw-p 00000000 00:00 0 [stack]
7ffefa571000-7ffefa574000 r--p 00000000 00:00 0 [vvar]
7ffefa574000-7ffefa576000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

因为可执行代码要调用 printf 函数,所以要在运行时链接 libc.so ,那么其最终是怎么使用到 printf 的呢,用 gdb 调试一下,可以看到其最终调用了跳入了地址 0x400410,进入了 puts 的 plt 表项:

1
2
3
4
5
6
7
8
9
10
11
(gdb) disassemble 
Dump of assembler code for function main:
0x000000000040052d <+0>: push % rbp
0x000000000040052e <+1>: mov % rsp,% rbp
=> 0x0000000000400531 <+4>: mov $0x4005d4,% edi
0x0000000000400536 <+9>: callq 0x400410 <puts@plt>
0x000000000040053b <+14>: mov $0x0,% eax
0x0000000000400540 <+19>: pop % rbp
0x0000000000400541 <+20>: retq
End of assembler dump.
(gdb)

因为可执行代码要调用 printf 函数,所以要在运行时链接 libc.so ,那么其最终是怎么使用到 printf 的呢,用 gdb 调试一下,可以看到其最终调用了跳入了地址 0x400410,进入了 puts 的 plt 表项,反汇编这个地址,结果如下:

1
2
3
4
5
6
(gdb) disassemble 0x400410
Dump of assembler code for function puts@plt:
0x0000000000400410 <+0>: jmpq *0x200c02 (% rip) # 0x601018 <puts@got.plt>
0x0000000000400416 <+6>: pushq $0x0
0x000000000040041b <+11>: jmpq 0x400400
End of assembler dump.

可以看到 0x40010 中是 plt 表中的内容,这里存放着三行代码。可以看到 plt 中又直接跳转到了 *0x200c02 (% rip) 中,跳转到 (0x200c02 + rip) 中存放的数据,接着看一下 (0x200c02 + rip) 中存放了什么,在 x86 汇编中 rip 是指向下一条指令的地址,所以是 0x601018,上面这条指令右边的注释已经帮我们标明了。

1
2
(gdb) print /x *0x601018
$1 = 0x400416

可以看到值为 0x400416, 正是当前指令的下一条指令 pushq $0x0。0x601018 存放的地址是 puts 函数的地址,如果链接器在初始化阶段已经初始化该项,并且将 puts 的地址填入该项,那么这个跳转指令就是我们所期望的,实现函数的正确调用,但是为了实现延迟绑定,链接器在初始化阶段并没有将 puts 的地址填入到该项,而是将上面 0x400416 指令 pushq 填入到了 puts@got.plt,所以我们看到 * 0x601018 的值为 0x400416。

0x400416 下面接着的指令是 jmpq 0x400400,用 x /5i 0x400400 指令 查看(不知道为啥 disassemble 指令不起作用,所以直接看内存):

1
2
3
4
5
6
7
(gdb) x /5i 0x400400
0x400400: pushq 0x200c02 (% rip) # 0x601008
0x400406: jmpq *0x200c04 (% rip) # 0x601010
0x40040c: nopl 0x0 (% rax)
0x400410 <puts@plt>: jmpq *0x200c02 (% rip) # 0x601018 <puts@got.plt>
0x400416 <puts@plt+6>: pushq $0x0
(gdb)

第一个 pushq 0x200c02 (% rip)link_map 的地址入栈,jmpq *0x200c04 (% rip) 跳转到 dl_runtime_reslove 中解析函数,解析完毕,再将解析到的函数地址,填到对应的 got 表项中。每个外部函数第一次调用都要进行这样一次函数的查找,并将地址填到 got 表项中,这样下次调用的时候,就无需查找了,直接跳转到外部函数。

1
2
(gdb) p /x *0x601018
$6 = 0xf7a80d60

5.7.7 动态库的编译

动态链接的基本思想就是把程序按照模块拆分为各个相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独可执行文件,当前 Linux 中,ELF 动态链接文件被称为动态共享对象(Dynamic Shared Objects),一般都是以.so 结尾,windos 下常见的就是.dll 结尾的文件。

我们依旧以一个例子展开描述动态链接的基本步骤。

程序 p1.c 如下:

1
2
3
4
5
6
7
#include "Lib.h"

int main(void)
{
foobar (1);
return 0;
}

程序 p2.c 如下:

1
2
3
4
5
6
7
#include "Lib.h"

int main(void)
{
foobar (2);
return 0;
}

上述两端代码都引用了 Lib.c 里面的 foobar 函数,为了在内存中加载一次 Lib.c ,使得 p1 和 p2 共享,将 Lib.c 编译成共享对象。

程序 Lib.c 如下:

1
2
3
4
5
6
#include <stdio.h>

void foobar(int a)
{
printf ("Printing from Lib.so % d\n", a);
}

首先使用命令 gcc -fpic -shared -o Lib.so Lib.c 将 Lib.c 编译为共享对象,-shared 表示产生共享对象,-fpic 表示产生地址无关代码(还有一个 –fPIC 区别在于这个参数产生的代码大一点,而 - fpic 产生的代码小一点,还有一点 - fpic 在某些平台上会有限制,比如全局符号的梳理或代码长度,而而 –FPIC 则没有此限制,一般情况下都用大写的来产生地址无关代码)然后我们得到了一个 Lib.so 的文件,然后我们分别编译链接 p1.c 和 p2.c

1
2
gcc -o p1 p1.c ./Lib.so
gcc -o p2 p2.c ./Lib.so

这样我们得到了两个可执行文件 p1 和 p2 ,基本过程如下:

img

与静态链接不同的地方在于 program1.o 和 Lib.o 会被链接在一起,产生可执行文件,但是动态链接的输入只有 program1.o ,在链接执行过程中,链接器会将 foobar 标记为一个动态链接的符号,不对它进行重定位,把这个过程留到装载时再执行,这就是要在编译时带上 Lib.so 的原因(Lib.so 内保存了完整的符号信息),执行 p1,输出如下:

1
2
$ ./p1     
Printing from Lib.so 1

在执行 p1 时,进程的虚拟地址空间布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ cat /proc/24125/maps
00400000-00401000 r-xp 00000000 08:13 2883957 /home/yw/mywork/my_programe/p1
00600000-00601000 r--p 00000000 08:13 2883957 /home/yw/mywork/my_programe/p1
00601000-00602000 rw-p 00001000 08:13 2883957 /home/yw/mywork/my_programe/p1
7f934331f000-7f93434dd000 r-xp 00000000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7f93434dd000-7f93436dd000 ---p 001be000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7f93436dd000-7f93436e1000 r--p 001be000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7f93436e1000-7f93436e3000 rw-p 001c2000 08:13 1310823 /lib/x86_64-linux-gnu/libc-2.19.so
7f93436e3000-7f93436e8000 rw-p 00000000 00:00 0
7f93436e8000-7f93436e9000 r-xp 00000000 08:13 2883937 /home/yw/mywork/my_programe/Lib.so
7f93436e9000-7f93438e8000 ---p 00001000 08:13 2883937 /home/yw/mywork/my_programe/Lib.so
7f93438e8000-7f93438e9000 r--p 00000000 08:13 2883937 /home/yw/mywork/my_programe/Lib.so
7f93438e9000-7f93438ea000 rw-p 00001000 08:13 2883937 /home/yw/mywork/my_programe/Lib.so
7f93438ea000-7f934390d000 r-xp 00000000 08:13 1310820 /lib/x86_64-linux-gnu/ld-2.19.so
7f9343af0000-7f9343af3000 rw-p 00000000 00:00 0
7f9343b0a000-7f9343b0c000 rw-p 00000000 00:00 0
7f9343b0c000-7f9343b0d000 r--p 00022000 08:13 1310820 /lib/x86_64-linux-gnu/ld-2.19.so
7f9343b0d000-7f9343b0e000 rw-p 00023000 08:13 1310820 /lib/x86_64-linux-gnu/ld-2.19.so
7f9343b0e000-7f9343b0f000 rw-p 00000000 00:00 0
7ffe39932000-7ffe39953000 rw-p 00000000 00:00 0 [stack]
7ffe3998d000-7ffe39990000 r--p 00000000 00:00 0 [vvar]
7ffe39990000-7ffe39992000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

可以看到整个进程的虚拟地址空间,多出来几个文件的映射,同时看到 p1 还引用了动态链接形式的 C 语言运行时库 libc-2.19.so ,还有一个对象是 ld-2.19.so,它是 Linux 下的动态链接器。动态链接器与普通共享对象一起被映射进进程的地址空间,在系统开始运行 p1 之前,首先会把控制权交给动态链接器,由其完成动态链接工作以后,再把控制权交给 p1。

Lib.so 的属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ readelf -l Lib.so 

Elf file type is DYN (Shared object file)
Entry point 0x5e0
There are 7 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x000000000000078c 0x000000000000078c R E 200000
LOAD 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
0x0000000000000238 0x0000000000000240 RW 200000
DYNAMIC 0x0000000000000e18 0x0000000000200e18 0x0000000000200e18
0x00000000000001c0 0x00000000000001c0 RW 8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
0x0000000000000024 0x0000000000000024 R 4
GNU_EH_FRAME 0x000000000000070c 0x000000000000070c 0x000000000000070c
0x000000000000001c 0x000000000000001c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
0x0000000000000200 0x0000000000000200 R 1

Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .jcr .dynamic .got

可以看到其加载段的虚拟地址为 0,需要在装载时由装载器根据当前地址情况,分配足够大小的虚拟地址空间给相应的共享对象。

如果当前可执行文件的所在的目录缺少 Lib.so,则会报错,执行 p1 时,会有 动态链接报错,这是一种常见的动态链接报错的形式,找不到动态库:

1
2
3
4
$ mv Lib.so Lib.so.bak
$ ./p1
./p1: error while loading shared libraries: ./Lib.so: cannot open shared object file: No such file or directory
$

5.8 显式运行时链接

支持动态链接的系统都支持一种更加灵活的模块加载方式,叫做显式运行时链接,也叫做运行时加载,也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。如果动态链接器可以在运行时将共享模块载入内存并且可以进行重定位操作,那么这种运行时加载在理论上也很容易实现,而且一般的共享对象不需要进行任何修改就可以进行运行时装载,这种共享对象往往被叫做动态装载库,和一般的共享对象没有什么区别。

对于 Linux 来讲,动态库跟一般的共享对象的主要区别在于共享对象是由动态链接器在程序启动之前负责装载和链接的,这一系列步骤由动态链接器自动完成,对于程序本身来讲是透明的;而动态链接库的装载则是通过一系列由动态链接器提供的 API 完成,详细可以参考 dlopen,dlsym,dlerror,dlclose 这几个函数的用法。

6. 加载

6.1 进程地址空间

Linux 进程地址空间由多个 segments 构成,不同的 segments 具备不同的属性(读、写、执行)以及具备不同的特征(静态、动态),完成不同的功能。

具体以 32bit 处理器为例,地址空间 layout 布局如下图所示:

img

从地址由小递增而看,分析各个 segments 具备的特征属性及实现功能,其中‘r’表示可读,‘w’表示可写,‘x’表示可执行,‘p’表示 private 私有,‘s’表示 shared 共享:

Text segment: 代码段,存放程序指令,通常为权限为‘rxp’

Data segment: 数据段,存放程序初始化后的全局及静态变量,通常为权限为‘rxp’

BSS segment: 数据段,存放程序未初始化或初始化为 0 的全局及静态变量,通常为权限为‘rxp’

以上三个 segments 的特征为静态的,即对于一个程序而言在编译后大小是固定的,所以这三个 segments 连续存储排布。

Heap segment: 堆,用于进程动态分配内存,通常为权限为‘rwp’

Mem map segment:Map 段,mmap 系统调用后申请的段,可以用于存储动态库、匿名页等等

Stack segment: 栈,用于函数调用过程栈生长及收缩,通常为权限为‘rwp’

对于一个进程实例,可以通过 cat /proc/{PID}/maps 查看该进程的内存地址空间布局,下图为一个进程实例:

img

上图包含了一个进程实例的以下关键信息:

  1. 进程地址空间 segments 构成
  2. 各 segments 占用的地址范围,具备的属性特征
  3. 各 segment 是匿名段还是命名段,如果是命名段,其指向的具体文件

6.2 程序加载过程

从编译链接的角度看,可以分为静态链接和动态链接。

同样,对于一个执行文件的加载运行过程,静态链接程序和动态链接程序存在差异:

  1. 静态链接程序:运行加载过程无需进行动态 lib 库的加载链接,静态链接程序运行时地址空间如下图所示:

img

  1. 动态链接程序:运行加载过程需要进行动态 lib 库的加载链接,动态链接程序运行时地址空间如下图所示:

img

程序运行过程主要完成以下三部分流程:

  1. 创建一个独立的虚拟地址空间。

  2. 读取可执行文件,并且建立虚拟空间与可执行文件的映射关系。

  3. 将 CPU 的 PC 寄存器设置成可执行文件的入口地址,启动运行新进程。

以下更为详细的介绍 ELF 可执行程序的加载流程:

  1. 读取并检查目标执行程序 ELF 头部

  2. 加载及解析目标程序的 Program Header

  3. 如果需要动态链接,则寻找和处理解释器段。INTERP 是解释器段类型,需要加载解析器段,用于加载共享库。找到后就根据其位置的 p_offset 和大小 p_filesz 把整个” 解释器” 段的内容读入缓冲区。

img

​ 通过命令 readelf -S 可以看到动态链接程序使用的解释器,如上图红框所示。而静态链接程序无此内容。

  1. 装入目标程序的段 segment

    从目标映像的程序头中搜索类型为 PT_LOAD 的段(Segment)。在二进制映像中,只有类型为 PT_LOAD 的段才是需要装入的。确定了装入地址后,建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射。

    有一点需要注意,在映射到进程的虚拟地址空间时,栈、堆、mmap、** 解析器 ** 段 的起始地址往往加上一个 随机偏移量。因为在 i386 系统上,文本基地址(.text)固定为 0x08048000,敏感的堆栈区域容易被推算出入口地址,从而被黑客攻击。

  2. 获取程序的入口地址

    完成了目标程序和解释器的加载,各个段的内容已经加载到内存了。

    1)如果需要装入解释器,则进入用户空间的入口地址设置成 l 解释器映像的入口地址。这样返回用户空间时先执行解析器程序,将需要的 share lib 映射到进程的 mmap 虚拟地址空间中;可通过 ldd 命令查看依赖动态库。

    2)若不需要装入解释器,那么这个入口地址就是目标映像本身的入口地址。

  3. 需要准备目标文件的参数环境变量等必要信息

    从 execve 系统调用拿到的参数、环境变量等等,还有一些 “辅助向量,经过一些设置后,压入进程栈中。

    这些信息需要复制到用户空间,使它们在 CPU 进入解释器或目标映像的程序入口时出现在用户空间堆栈上。

进程加载段(segment)的类型定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define PT_NULL    0	             /* 未定义的条目 */
#define PT_LOAD 1 /* 可加载的段 */
#define PT_DYNAMIC 2 /* 动态链接相关信息 */
#define PT_INTERP 3 /* 解析器段 */
#define PT_NOTE 4 /* 附加信息的位置和大小 */
#define PT_SHLIB 5 /* 预留类型,无意义 */
#define PT_PHDR 6 /* 指出该程序头表在文件和内存映像中的位置和大小 */
#define PT_TLS 7 /* Thread local storage segment */
#define PT_LOOS 0x60000000 /* OS-specific */
#define PT_HIOS 0x6fffffff /* OS-specific */
#define PT_LOPROC 0x70000000 /* 体系相关信息 */
#define PT_HIPROC 0x7fffffff /* 体系相关信息 */
#define PT_GNU_EH_FRAME 0x6474e550 /* 供栈回溯的信息 */
#define PT_GNU_STACK (PT_LOOS + 0x474e551) /* 栈 */

7. 编译、链接工具使用举例

gcc

以下是我们 TDMP 平台的一条编译命令,输入文件为 specialDial.c,输出为 specialDial.o:

1
arm-openwrt-linux-muslgnueabi-gcc -Os -pipe -march=armv7-a -mtune=cortex-a7 -g3 -fno-caller-saves -Wa,--noexecstack -fhonour-copts -mfloat-abi=soft -fPIC -fstack-protector -D_FORTIFY_SOURCE=2 -Wl,-z,now -Wl,-z,relro -DMANUFACTURER_VENDOR_TPLINK -DTP_MESH_TPLINK -DINCLUDE_TPDDNS_FUNC -DVENDOR_WLAN_DRIVER_qca -DVENDOR_ETHERNET_ipq5018 -DCONFIG_LAN_ETH_NAME_PREFIX=\"eth0.\" -DCONFIG_APS_BUILT_IN -DCONFIG_PS_FAST_FORWARD_SUPPORT -DINCLUDE_APP_MARKET -DFACBOOT_UPGRADE_SUPPORT -DTP_FEATURE_MESH -DTP_WPS_ENROLLEE_SUPPORT -DTP_FEATURE_DFS_SUPPORT -DMESH_WPS_STA_SUPPORT -DWIFISON_FACTORY_PAIR -DTP_WPS_FRAG_SUPPORT -DTP_FEATURE_DUALBAND_BH -DTP_FEATURE_WPS -DINET6 -DHTTP_IPV6_MANAGEMENT -DCONFIG_STATISTICS_IPV6_SUPPORT -DIPV6_AUTO_DIAL -DCONFIG_SINGLE_WAN_NAT66 -DCONFIG_IPV6_DIAL_FOLLOW_IPV4 -DCONFIG_DNS_PROXY_IPV6_SUPPORT -DGPIO_SYS_LED=38 -DGPIO_MESH_LED_RED=19 -DGPIO_PAIR_BUTTON=31 -DCONFIG_GPIO_LED_ACTIVE_MODE=0 -DGPIO_RESET_BUTTON=32 -DTP_FEATURE_EVENT_ENHANCE -DTP_FEATURE_EVENT_ENHANCE_IN_INETD -DHTTP_MAX_CONTENT=0x1200000 -DCONFIG_VLAN_PER_PORT -DCONFIG_WAN_AT_NOPORT -DWLAN_5G_BAND1_SUPPORT -DWLAN_2G_11AX_SUPPORT -DMAPD_SUPPORT_802_11_AX -DWLAN_5G_11AX_SUPPORT -DMAPD_SUPPORT_802_11_AX -DWLAN_OFDMA_DEFAULT_CONFIG_DISABLED -DWLAN_5G_BAND1_BAND2_SUPPORT -DWLAN_5G_BW160_SUPPORT -DWLAN_CONFIG_BANDWIDTH_SUPPROT_160M -DPHY_SPEED_1000M -DTP_FEATURE_WAN_PORT_DETECT -DFIX_WAN_PORT=3 -DSWITCH_PHY_NUM=4 -DTP_FEATURE_POWER_POSITION_LEFT -DDM_ARRAY_OPTION_SUPPORT -DTP_WAN_PORT_DETECT_PROTOCOL=0x7878 -DDMS_PLUGIN_BUILTIN -DDEV_MAIN_VER=0x50010000 -DDEV_MINOR_VER=0x0000 -DSWITCH_PORT_MASK=0xF -DLAN_WAN_PARTITION_BY_VLAN_TAG -DWLAN_DUAL_BAND -DGUEST_5G_SUPPORT -DGUEST_SSIDBRD_SUPPORT -DWLAN_SECURITY_SUPPORT -DWLAN_DOT11_SAE -DWLAN_MULTI_SSID_SUPPORT -DWLAN_WIFI5_COMPATIBLE_BSS_SUPPORT -DWLAN_WIFI5_BSS_DEFAULT_SUFFIX=\"_WiFi5\" -DMAP_BSS_MAX_NUM=6 -DWLAN_EXTEND_MSSID_NUM=3 -DCTCFG_WLAN_SWITCH_SUPPORT -DUSE_DHCP_DETECT=1 -DCTCFG_TP_FEATURE_RSSI_DETECT -DBUILD_DATE=220828 -DBUILD_DATE_YEAR=22 -DBUILD_DATE_MON=8 -DBUILD_DATE_MDAY=28 -DBUILD_DATE_HOUR=11 -DBUILD_DATE_MIN=5 -DBUILD_DATE_SEC=7 -DCONFIG_PS_PORT_MAX=32 -DCTCFG_SUPPORT -DPLATFORM_TDMP -DDUAL_FREQ -DCTCFG_WAN_BRIDGE -DDEFAULT_SYS_MODE=0 -DCTCFG_NETSTAT_SUPPORT -DCTCFG_ROLE_SWITCH_SUPPORT -DCTCFG_MAPD_SERVER_DOMAINS=\"wifiserver.smartont.net:NULL\" -DCONFIG_CUEI_SUPPORT -DELINK_SUPPORT -DELINK_SYNC_DISABLE -DCTCFG_WLAN_EXT -DCTCFG_TRAFFIC_STATISTIC_EX -DCTCFG_MAC_FILTER_SUPPORT -DCTCFG_IPTV_SUPPORT -DIPTV_LAN1_PORT_INDEX=2 -DIPTV_LAN2_PORT_INDEX=1 -DIPTV_LAN3_PORT_INDEX=0 -DELINK_V1 -DCT_WOCLIENT_SUPPORT -DWOLINK_FEATURE -DCT_SOHO_SDK_API -DCT_SPEED_LIMIT_BITS -DHAVE_CYASSL -DCT_WOCLIENT_SCRAM_SUPPORT -DDUAL_IMAGE -DUSE_NAND_FLASH -DCONFIG_MTD_SPI_NOR_UC_USE_4K_SECTORS -DCTCFG_GET_LAN_BY_MAC -DCPU_INFO_SUPPORT -DCTCFG_TELNET_SUPPORT -DCTCFG_TELNET_USERNAME=\"useradmin\" -DCTCFG_TELNET_PASSWD=\"123456\" -DCTCFG_TELNET_ENABLE=0 -DDMS_FEATURE_ATED_FIRSTBOOT_LIMIT -DDMS_FEATURE_TMP_FIRSTBOOT_LIMIT -DCTCFG_FACTORY_TEST_STATUS_SUPPORT -DCT_WPS_PIN_BAND_SUPPORT -DCTCFG_LAN_CONNECT_TRIGGER_PAIRING -DTP_FEATURE_REMOTE_DM -DCONFIG_USE_kmod_wlan_access_mng_notify -DCTCFG_MULTI_DOMAIN_SUPPORT -DFPIVOT_PATH_LOCK -DCONFIG_SET_SWITCH_FC_PARAM_BY_PORT_NUM -DTP_FEATURE_CFG80211 -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/staging_dir/target-arm_cortex-a7_musl-1.1.16_eabi/usr/include/libnl -DTP_FEATURE_ANI_ENHANCE -DTP_FEATURE_ANI_DESENSE_LEVEL_LOWER_BOUND=-5 -DTP_FEATURE_ANI_DESENSE_LEVEL_UPPER_BOUND=25 -DTP_FEATURE_ANI_DESENSE_LEVEL_ERROR_SCALE_MASK=0x00880004 -DTP_FEATURE_ANI_DYNAMIC_EDCCA -DTP_FEATURE_ANI_DYNAMIC_EDCCA_UPPER_BOUND=0x26 -DTP_FEATURE_ANI_DYNAMIC_EDCCA_SCALE_MASK=0x1 -DTP_FEATURE_ANI_DYNAMIC_NF -DTP_FEATURE_ANI_DYNAMIC_NF_THESHOLD=15 -DTMP_WLAN_INFO_SUPPORT -DSUPPORT_ENTER_ART -DCONFIG_LOCAL_MAX_STA_NUM=256 -DCONFIG_REMOTE_MAX_STA_NUM=256 -DCONFIG_MANAGE_MAX_STA_NUM=128 -DCONFIG_TOPOLOGY_MAX_STA_LIMIT=256 -DHOST_NUM_2G=128 -DHOST_NUM_5G=128 -DWLAN_BAND_MAX_STA_NUM=128 -DWAN_MAX_PHY_NUM=1 -DTOPOLOGY_UPDATE_AP_PHY_ATTR_BY_NOTIFY -DCONFIG_RTNL_LOCAL_FDB_HANDLE -DCONFIG_GET_SWITCH_PORT_FLOW_STAT -DCONFIG_SUPPORT_QCA_NSS -DCTCFG_FAC_MAC_BOTH_USE_FOR_LAN_WAN -DCONFIG_DEFAULT_QUERY_ENABLE_VALUE -ffunction-sections -fdata-sections -Werror=implicit-function-declaration -Werror=format-extra-args -Werror=switch -Werror=implicit-int -Werror=return-type -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/include/-I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/staging_dir/target-arm_cortex-a7_musl-1.1.16_eabi/tdmp/usr/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/staging_dir/target-arm_cortex-a7_musl-1.1.16_eabi/usr/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/staging_dir/target-arm_cortex-a7_musl-1.1.16_eabi/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/staging_dir/toolchain-arm_cortex-a7_gcc-5.2.0_musl-1.1.16_eabi/usr/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/staging_dir/toolchain-arm_cortex-a7_gcc-5.2.0_musl-1.1.16_eabi/include  -fpic -Wall -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/framework/standardApi -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/staging_dir/target-arm_cortex-a7_musl-1.1.16_eabi/tdmp/usr/include -DcreationDate="\"Aug 28 2022, 11:13:39\"" -DEXCLUDE_RADIUS -DTP_FEATURE_STEER -DWLAN_DUAL_BAND -DLINUX -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/libs/libct/ct_sohojsonapi/src/soho_json_api_impl/sdmp/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/libs/libct/ct_sohosdkapi/src/platform/sdmp/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/libs/libct/ct_sohojsonapi/src/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/libs/libct/ct_utilitylib/src/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/libs/libct/ct_sdk_api/src/platform/sdmp/include -I/var/lib/jenkins/workspace/soho4_tdmp_qca_ax3000_release/torchlight/build_dir/target-arm_cortex-a7_musl-1.1.16_eabi/dms/libs/libct/ct_sdk_api/src/platform/sdmp/soho_json_api_impl/include -c -o specialDial.o specialDial.c

看此很长,但很多是重复项,一般来说,我们需要重点关心的有以下几项:

  • -Os,编译优化等级,介于 -O2-O3 之间,如果遇到问题,怀疑是编译器优化造成的,可以将其改为 - O0。
  • -Wa,开启所有警告,平时要重视所有的警告信息。
  • -Wl,-z,now -Wl,两个 -Wl,代表中间的参数是需要传递给链接器的参数。
  • -D,一些宏的定义,一般通过变量 CFLAGS 传进来。
  • -I,头文件的搜索路径,当我们写 #include xxx.h 时,编译时需要将xxx.h的路径通过 -Ipath 告诉编译器,一般也是通过变量 CFLAGS 传进来的,如果找不到头文件,优先考虑排查这个参数。
  • -o,输出文件的名字

ld

以下是 SDMP 平台链接命令,链接生成最终的可执行文件:

1
2
ldarm -EL -X -N -gc-sections -e sysInit -Ttext 40205000 D:\jenkins\workspace\soho4_sdmp_mtk_arm_non_smart_router_release/image/mt7626/vxbin/basicRouter/dataSegPad.o D:\jenkins\workspace\soho4_sdmp_mtk_arm_non_smart_router_release/image/mt7626/vxbin/basicRouter/partialImage.o \
D:\jenkins\workspace\soho4_sdmp_mtk_arm_non_smart_router_release/image/mt7626/vxbin/basicRouter/ctdt.o -T D:/jenkins/workspace/Tornado/Toolchain_4.1.2_ARMv7_Build_SDMP_Kernel_Mesh_IPv6_T4/target/h/tool/gnu/ldscripts/link.RAM -o D:\jenkins\workspace\soho4_sdmp_mtk_arm_non_smart_router_release/image/mt7626/vxbin/basicRouter/vxWorks

平时我们基本不会遇到该命令相关的错误,但还是了解一下其中的参数:

  • -EL,小端模式,对应的,-EB 为大端模式。
  • -X,不保留临时符号。
  • -gc-sections 去除没有用到的 sections,该选项配合编译选项 - ffunction-sections -fdata-sections,可以去除没有被调用的函数和数据,节省 flash 空间。
  • -e 设置第一个函数。
  • -Ttext 40205000 设置 text 段的其实地址为 0x40205000 。
  • -T D:/jenkins/workspace/Tornado/Toolchain_4.1.2_ARMv7_Build_SDMP_Kernel_Mesh_IPv6_T4/target/h/tool/gnu/ldscripts/link.RAM 链接脚本,链接脚本上文已经有介绍,这里不再介绍。
  • -o D:\jenkins\workspace\soho4_sdmp_mtk_arm_non_smart_router_release/image/mt7626/vxbin/basicRouter/vxWorks 最终生成的文件。

参考资料:

《程序要的自我修养》