工具链

软件的编译过程由一系列的步骤完成,每一个步骤都有一个对应的工具。这些工具紧密第工作在一起,前一个工具的输出是后一个工具的输入,像一根链条一样,因此,人们也把这些工具的组合形象地称为工具链。

在本书中,我们将从源码开始,逐步构建一个基本的Linux操作系统。显然,工具链是我们首先需要考虑的,因为工具链是编译包括内核在内的操作系统各个组件的基础。

编译过程

在Linux上通常只需使用gcc就可以完成整个编译过程。但事实上,gcc并不是一个编译器,而是一个驱动程序。在整个编译过程中,gcc就像一个导演一样,编译过程中的每一个环境由具体的组件复杂,如编译过程由ccl复杂、汇编过程由as复杂、链接过程由ld负责。

可以通过如下命令观察一个完成的编译过程中的步骤:

$ echo "main(){}" > main.c
$ gcc -v main.c

对于一个C程序来说,从源代码构建出可执行程序经历了三个阶段:

  • 编译,gcc调用编译器ccl进行编译,产生的汇编代码保存在目录/tmp下的.s文件中。
  • 汇编,gcc调用汇编器as,产生的目标文件保存在/tmp目录的.o文件中。
  • 链接,gcc调用collect2进行链接,collect2是一个辅助程序,最终它仍将叫用链接器ld完成真正的链接过程。

对于C程序来说,编译过程也可以拆分为两个阶段:预编译和编译。所以软件构件过程通常分为四个阶段:预编译、编译、汇编以及链接。

预编译

gcc 中提供的预编译器为cpp。ccl也包含了预编译的功能。

文件foo.h:

#ifndef _FOO_H_
#define _FOO_H_

#define PI 3.1415926
#define AREA

struct foo_struct {
    int a;
}
#endif

文件hello.c:

#include "foo.h"

int main(int argc, char*argv[])
{
    int result;
    int r = 5;

#ifdef AREA
     result = PI * r * r;
#else
    result = PI * r * 2;
#endif
}

执行命令:

$ gcc -E hello.c -o hello.i

预编译后的结果保存在文件hello.i中,其内容如下:

struct foo_struct {
    int a;
}

int main(int argc, char*argv[])
{
 int result;
 int r = 5;
 result = 3.1415926 * r * r;
}

典型的预编译指令按照如下方式进行处理:

  • 文件包含,文件包含命令知识预编译器将一个源文件的内容全部复制到当前源文件中。
  • 宏定义,预编译器将宏名替换为具体的值。
  • 条件编译,按照一定的条件去编译源程序的不同部分。

编译

编译程序对预处理过的结果进行词法分析、语法分析、语义分析,然后生成中间代码,并对中间代码进行优化,目标是使最终生成的可执行代码执行时间更短、占用的空间更小,最后生成相应的汇编代码。

文件foo2.c:

int foo2 = 20;
void foo2_func(int x)
{
    int ret = foo2;
}

使用如下命令进行编译,不进行汇编和链接:

$ gcc -S foo2.c

编译后产生的汇编文件为foo2.s,内容如下:

    .file    "foo2.c"
    .globl    foo2
    .data
    .align 4
    .type    foo2, @object
    .size    foo2, 4
foo2:
    .long    20
    .text
    .globl    foo2_func
    .type    foo2_func, @function
foo2_func:
.LFB0:
    .cfi_startproc
    pushl    %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    subl    $16, %esp
    movl    foo2, %eax
    movl    %eax, -4(%ebp)
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size    foo2_func, .-foo2_func
    .ident    "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    .section    .note.GNU-stack,"",@progbits

foo2.s中相当一部分是汇编器的伪指令。伪指令不参与CPU运算,只指导编译链接过程。

每个函数调用过程中,都会形成一个栈帧。frame pointerbase pointer均指向栈帧的底部,只是叫法不同。

理论上,调试器或异常处理程序完全可以根据frame pointer来遍历调用过程中各个函数的栈帧,但是因为gcc的代码优化,可能导致调试器或异常处理很难甚至不能正常回溯栈帧,所以这些伪指令的目的就是负责编译过程创建栈帧信息,并将它们保存在目标文件的段.eh_frame中,这样就不会被编译器优化影响了。去除伪指令之后函数foo2_func中CPU真正执行的代码如下:

1    pushl    %ebp
2    movl    %esp, %ebp
3    subl    $16, %esp
4    movl    foo2, %eax
5    movl    %eax, -4(%ebp)
6    leave
7    ret

在汇编语言中,在函数的开头和结尾处分别会插入一小段代码,分别称为Prologue和Epilogue,如果foo2_fun中的第1、2、3行代码就是Prologue,第6、7行代码就是Epilogue。

Prologue保存主调函数的frame pointer,这是为了在子函数调用结束后,恢复主调函数的栈帧。同时为子函数准备栈帧。其主要操作包括:

  • 保存主调函数的frame pointer。在推出子函数时可以从栈中恢复主调函数的frame pointer。
  • 将esp赋值给ebp,即将子函数的frame pointer指向主调函数的栈顶。
  • 修改栈顶指针esp,为子函数的本地变量分配空间。

Epilogue功能与Prologue恰恰想法,其主要操作包括:

  • 将栈指针esp指向当前子函数的栈帧的frame pointer,也就是说,指向当前栈帧的栈底,而在这个位置,恰恰时Prologue保存的主调函数的frame pointer。然后通过指令pop将主调函数的framepointer弹出到ebp中,
  • 将调用子函数时call指令压栈的返回低战从栈顶pop搭配EIP中,并挑战到EIP处继续执行。

汇编

汇编器将汇编代码翻译为机器指令,在目标文件中创建负责链接时需要的信息,包括符号表、重定位表等。

目标文件

汇编过程的产物时目标文件。Linux下的目标文件采用ELF格式存储。Linux下的二进制文件包括可执行文件、静态库和动态库等,均采用ELF格式存储。

对于32位的ELF文件来说,其最前部时文件头部信息,描述了整个文件的基本数学,除了包括文件运行在什么操作系统中、运行在什么硬件体系结构上、程序入口地址是什么等基本信息外,最重要的是记录了两个表格的相关信息:

  • Section Header Table,主要是共编译时链接使用的,表格中定义了各个段的位置、长度、属性等信息。
  • Program Header Table,主要是供内核和动态加载器从磁盘加载ELF文件到内存时使用的。对于目标文件,由于只是编译过程的一个中间产物,不涉及装载运行,因此,在目标文件中不会创建Program Header table.

文件hello.c:

#include <stdio.h>

extern int foo2;

int main(int argc, char *argv[])
{
    foo2 = 5;
    foo2_func(50);
    return 0;
}

文件foo1.c:

int foo1 = 10;

void foo1_func()
{
    int ret = foo1;
}

运行:

$ gcc -c hello.c foo1.c foo2.c
$ readelf -h foo2.o
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32 # 该文件是32位的ELF文件
  Data:                              2's complement, little endian # 使用小端字节序存储字节
  Version:                           1 (current) 
  OS/ABI:                            UNIX - System V # API遵循 UNIX-System V 标准,运行在类UNIX系统上
  ABI Version:                       0
  Type:                              REL (Relocatable file) # 类型
  Machine:                           Intel 80386 # 目标文件位IA32 架构编译的
  Version:                           0x1
  Entry point address:               0x0 # 目标文件,不存在执行的概念,所有入口在这里不适用
  Start of program headers:          0 (bytes into file) # 目标文件,同上
  Start of section headers:          260 (bytes into file) # Section Header Table在便宜260字节处。
  Flags:                             0x0
  Size of this header:               52 (bytes) # ELF 头占用了52个字节
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes) # Section Header Table中的每个Section Header占用40字节
  Number of section headers:         12 # Section Header Table 共包含12个Section Header
  Section header string table index: 9

ELF的类型:

  • 可执行文件的类型是“EXEC(Executable file)”
  • 动态库的类型是“DYN(Shared object file)”
  • 静态库和目标文件的类型是“REL(Relocatable file)”

文件头信息后,就是各个段了。好不夸张地说,ELF文件就是段的组合。大体上,段可以分为如下几类:

  • 存储指令的,通常称为代码段
  • 存储数据的,通常称为数据段
    • 以及初始化的全局数据存放在".data"段中
    • 未初始化的全家数据存储在“.bss”段。
  • 辅助链接段,汇编器在目标文件中创建用于存储如符号表、重定位表等。

查看段信息:

$ readelf -S foo2.o 
There are 12 section headers, starting at offset 0x104:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000010 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 00039c 000008 08     10   1  4
  [ 3] .data             PROGBITS        00000000 000044 000004 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000048 000000 00  WA  0   0  4
  [ 5] .comment          PROGBITS        00000000 000048 00002b 01  MS  0   0  1
  [ 6] .note.GNU-stack   PROGBITS        00000000 000073 000000 00      0   0  1
  [ 7] .eh_frame         PROGBITS        00000000 000074 000038 00   A  0   0  4
  [ 8] .rel.eh_frame     REL             00000000 0003a4 000008 08     10   7  4
  [ 9] .shstrtab         STRTAB          00000000 0000ac 000057 00      0   0  1
  [10] .symtab           SYMTAB          00000000 0002e4 0000a0 10     11   8  4
  [11] .strtab           STRTAB          00000000 000384 000017 00      0   0  1

根据输出可见,目标文件的Section Header Table中包含12个SectionHeader:

  • .text,不要将".text"段和进程的代码段混淆,进程的代码段不仅能包括“.text”段,在后面链接时,我们还会看到,包括.init、.fini等段存储的代码都术语代码段。这些段都被映射到Program Header Table中的一个段,在ELF加载时,统一作为进程的代码段。
  • .data
  • .bss, 虽然目标文件的Section Header Table中包含“.bss”段,但是因为其不必记录数据,所以".bss"段在文件中只占据Section Header Table中的一个SectionHeader,而并没有对应的段。在加载程序时,加载器将依据.bss段的Section Header中的信息,在内存中为其分配空间。
  • .symtab 记录的是符号表。因为符号的名字字串长度可变,所以目标文件将符号的名字字符串玻璃出来,记录在另外一个段“.strtab”中,符号表使用符号名字的索引在段“.strtab”中的偏移来确定符号的名字。
  • 同样的道理, “.shstrtab”中记录的是段的名字。
  • 以“rel”开头的,记录的是段中需要重定位的符号。
  • “.eh_fram”段中记录的是调试和异常处理时用到的信息。
  • ".comment"、".note.GNU-stack"等段如期名字所示,都是一些"comment"和“note”,无论时链接还是封装都不会用到,我们不必关心

翻译机器指令

机器指令由操作码和操作数组成:

  • 操作码知名该指令索要完成的操作,即指令的功能
  • 操作数是参与操作的数据,主要以寄存器或存储器地址的形式指明数据的来源或者计算结果存放的位置等。

汇编过程就是将助记符翻译为对应的以0和1表示的机器指令,我们也将其称为操作码和操作数的编码过程。对于IA32,其机器指令的格式如下:

操作码Opcode直接嵌在指令中,操作码的翻译过程相对简单,将汇编指令中的操作码助记符翻译为相应的操作码即可,操作码助记符与操作码的对应关系可根据CPU的指令手册确定。

将操作数助记符翻译为操作数的机器码相对要复杂一些,操作数并没有直接嵌在指令编码中,而是根据汇编指令使用的具体寻址方式,设置ModR/M、SIB、Displacement和Immediate各项的值,这个过程称为操作数的编码。CPU根据ModR/M、SIB、Displacement和Immediate的值,解码出操作数。

典型的操作数的编码方式包括下面几种

  • 操作数地址通过ModR/M中的Mod+R/M指定。
  • 操作数通过ModR/M中的Reg/Opcode指定。
  • 操作数直接嵌入机器指令中。汇编指令中直接使用了操作数的地址,即所谓的直接寻找方式。
  • 操作数直接嵌入在指令中。操作数就是参与计算的数据,即所谓的立即寻找。
  • 操作数隐含在Opcode中。保存操作数的寄存器直接隐含在操作码Opcode中,即所谓的隐含寻址。

重定位表

在进行汇编时,在一个模块内,如果引用了其他模块或库中的变量或者函数,汇编器并不会解析引用外部符号。因为在汇编时,模块时独立编译的,所以对于引用的外部的符号一无所知。因此,在目标文件的机器指令中,汇编器基本上是留空引用的外部符号的地址。然后,在链接时,在符号地址确定后,链接器再来修订这些位置,这个修订过程被称为重定位。事实上,为了复杂链接器在链接时计算修订之,这些需要修订的位置并不是全部都置为0,有时这些填充的是一个Addend。

但是链接器并不能聪明到可以自动找到目标文件中引用外部符号的地方,所以在目标文件中需要建立一个表格,这个表格中的第一条记录对应的就是一个需要重定位的符号,这个表格通常称为重定位表。汇编器将为可重定位文件中每个包含需要重定位符号的段都建立一个重定位表。

符号表

既然在链接时,需要重定位目标文件中引用的外部符号,系哪人,链接器需要指导这些符号的定义在哪里,为此汇编器在每个目标文件中创建了一个符号表,符号表中记录了这个模块定义的可以共给其他模块引用的全家符号。

$ readelf -s foo2.o 

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

除了模块定义的符号外,符号表也包括模块引用的外部符号。

链接

链接器将一个或多个目标文件和库,包括动态库和静态库,链接为一个单独的文件。链接器的动作可以分为两个阶段:

  • 第一阶段是将多个文件合并为一个单独的文件。对于可执行文件,还需要为指令及符号分配运行时地址。
  • 第二阶段进行符号重定位。

合并目标文件

合并多个目标文件其实就是将多个目标文件的相同类型的段合并到一个段中。

符号重定位

链接时,第一阶段完成后,目标文件依据合并完成,并且依据为符号分配了运行时地址,链接器将进行符号重定位。

链接静态库

静态库其实就是多个目标文件的打包,因此,与合并多个目标文件并无本质差别。但是有一点需要特别说明,在连接静态库时,并不是将整个静态库中包含的目标文件全部复制一份到最终的可执行文件中,而是仅仅链接库中使用的目标文件。

链接动态库

动态库不会再可执行文件中有任何副本,那么为什么编译链接时需要指定动态库呢?

  • 动态加载器需要指导可执行程序依赖的动态库,这样再加载可执行程序时才能加载其依赖的动态库。所以,在链接时,连机器将根据可执行程序引用的动态库中的符号的情况在dynamic段中记录可执行程序依赖的动态库。
  • 链接器需要在重定位表中创建重定位记录,这样当动态链接器加载hello时,将依据重定位记录重定位hello引用的这些外部符号。重定位记录存储在ELF的重定位段中,ELF文件中可能有多个段包含需要重定位的符号,所以可能会包含多个重定位段。

构建工具链

虽然构建的目标系统是运行在IA32体系构架上的,但是我们不能使用宿主系统的工具链,否则可能会导致目标系统依赖宿主系统。同时,目标系统使用的工具链的各个组件的版本,通常不同于宿主系统,因此,这也要求为目标系统构建一套新的工具链。

GNU 工具链组成

编译分为4个阶段,分别是:编译预处理、编译、汇编以及链接。每个阶段都涉及了若干工具,GNU将这些工具分别包含在3个软件包中:Binutils、GCC、Glibc。

  • Binutils:GNU将凡是与二进制文件相关的工具,都包括在软件包Binutils中。主要包括as/ld/objdump/strip/cpp。
  • GCC:GNU将编译器包含在GCC中,包括C编译器、C++编译器等。GCC中还提供了C++的启动文件。
  • Glibc:C库包含在Glibc中。除了C库外,动态链接器也包含在这个包中。另外这个包中还提供C的启动文件。

除了这三个软件包外,工具链中还需要包括内核头文件。用户空间中的很多操作需要借助内核来完成,但是通常用户程序不必直接和内核打交道,而是通过更易用的C库。C库中的很大一部分函数是对内核服务的封装。在某种意义上,内核头文件可以看作是内核与C库之间的协议。因此,构建C库之前,需要首先在工具链中安装内核头文件。

构建工具链的过程

GNU 将编译器和C库分开放在两个软件包里,好处是比较灵活,在工具链中可以选择不同的C库。但是,也带来了编译器和C库的循环依赖问题:编译C库需要C编译器,但是C编译器也依赖C库。虽然理论上编译器不应该依赖C库,C编译器值复制将源代码翻译为汇编代码,但是事实并非如此:

  • C 编译器需要指导C库的某些特性,一次来决定支持哪些特性。
  • C++的库和编译器需要C库支持,比如异常处理部分和栈回溯部分。
  • GCC不仅包含编译器,还包含一些库,这些库通常依赖C库。
  • C 编译器本身也会使用C库的一些函数。

C99标准定义了两种运行环境

  • hosted environment:针对的是具有操作系统的环境,程序一般是运行在操作系统之上的,因此这个系统不仅是内核,还包括外围的C库。
  • freestanding environment:程序需要额外环境的支持,直接运行在逻辑,比如Linux内核。不再依赖操作系统内核和C库,所有的功能都在单个程序的内部实现。

针对这两种运行环境,C99标准分别定义了两种实现:

  • hosted implementation:支持完成的C标准库,通常包含编译器和C库。
  • freestanding implementation:支持完成的语言标准,但是只要求支持部分库标准。通常只包含编译器。

C99标准要求hosted implementation支持 freestanding implementation。

工具链构建步骤:

  1. 构建交叉Binutils。
  2. 构建临时的交叉编译器(仅支持freestanding)。
  3. 安装目标系统的内核头文件。
  4. 构建目标系统的C库。
  5. 构建完整的交叉编译器。

准备工作

新建普通用户vita:

$ groupadd vita
$ user add -m -s /bin/bash -g vita vita

在/etc/sudoers.d目录下添加一个文件vita,内容如下:

vita ALL=(ALL) NOPASSWD:ALL

建立工作目录:

$ sudo mkdir /vita
$ sudo chown -R vita:vita /vita
$ cd /vita
$ mkdir source \ # 存放源代码
    build \ # 用作编译
    cross-tool \ # 保存交叉编译工具
    cross-gcc-tmp \ # 保存临时的freestanding编译器
    sysroot # 编译好的目标机器上的文件安装在sysroot目录下,sysroot目录相当于目标系统的跟文件系统。

定义环境变量:

$ echo '
unset LANG # 避免中文环境带来影响
export HOST=i686-pc-linux-gnu # 
export BUILD=$HOST
export TARGET=i686-none-linux-gnu
export CROSS_TOOL=/vita/cross-tool
export CROSS_GCC_TMP=/vita/cross-gcc-tmp
export SYSROOT=/vita/sysroot
PATH=$CROSS_TOOL/bin:$CROSS_GCC_TMP/bin:/sbin:/usr/sbin:$PATH # 将交叉编译工具链的目录条件到PATH中
' >> ~/.bashrc

Binutils、GCC以及Glib的配置脚本中均包含三个配置参数:HOST/BUILD/TARGET,这三个参数的值均是大致形如ARCh-VENDOR-OS三元组的组合。如果不显示指定这几个参数,编译脚本将自动探测编译所在的机器的相关值。可以通过查看变量MACHTYPE查看机器的三元组。

如果HOST的值和TARGET的值相同,那么编译脚本就构建本地编译工具。只有当HOST和TARGET的值不同时,编译脚本才构建交叉编译工具。因此,将TARGET的值设置为i686-none-linux-gnu

构建二进制工具

构建:

$ cd /vita/build
$ tar xvf ../source/binutils-2.23.1.tar.bz2
$ mkdir binutils-build
$ cd binutils-build
$ ../binutils-2.23.1/configure \
    --prefix=$CROSS_TOOL \ # 指定安装脚本将编译好的二进制工具安装到$CROSS_TOOL
    --target=$TARGET \ # 没有指定--host和--build,系统将自动探测,与指定的不同将编译“运行在本机,但是最后编译链接的程序时运行在$TARGET上”的交叉二进制工具。
    --with-sysroot=$SYSROOT # 告诉链接器,目标系统的根文件系统的目录,链接时去该目录寻找相关的库
$ make
$ make install

安装内容:

  1. 二进制工具,安装在$CROSS_TOOL/bin目录下
  2. 链接脚本,安装目录是 $CROSS_TOOL/i686-none-linux-gno/lib/ldscripts
  3. 编译器内部使用的二进制工具,安装目录是 $CROSS_TOOL/i686-none-linux-gnu/bin,与$CROSS_TOOL/bin目录的工具完全相同,只是名称不同而已。

编译 freestanding 的交叉编译器

编译 freestanding 的交叉编译器:

$ cd /vita/build
$ tar xvf ../source/gcc-4.7.2.tar.bz2
$ cd gcc-4.7.2/
$ tar xvf ../../source/gmp-5.0.5.tar.bz2 
$ mv gmp-5.0.5/ gmp
$ tar xvf ../../source/mpfr-3.1.1.tar.bz2 
$ mv mpfr-3.1.1/ mpfr
$ tar xvf ../../source/mpc-1.0.1.tar.gz
$ mv mpc-1.0.1/ mpc
$ cd ..
$ mkdir gcc-build
$ cd gcc-build/
$ ../gcc-4.7.2/configure \
    --prefix=$CROSS_GCC_TMP \ # 安装在一个临时目录
    --target=$TARGET \ # 告诉编译脚本构建交叉编译器
    --with-sysroot=$SYSROOT \ # 指定根文件系统
    --with-newlib --enable-languages=c \ # 只使用c语言
    --with-mpfr-include=/vita/build/gcc-4.7.2/mpfr/src \
    --with-mpfr-lib=/vita/build/gcc-build/mpfr/src/.libs \
    --disable-shared \ # 默认编译静态库和动态库版本的libgcc,但C库尚未编译,不能链接动态库
    --disable-threads \ # 关闭其他一些不需要的特性
    --disable-decimal-float --disable-libquadmath \
    --disable-libmudflap --disable-libgomp \
    --disable-nls --disable-libssp
$ make
$ make install

--disable-shared 禁止编译libgcc的动态库后,GCC的编译脚本将不再编译库libgcc_eh.a。但是后面编译Glibc时,将链接libgcc_eh.a,可以通过一个指向libgcc.a的符号链接libgcc_eh.a来解决这个问题:

$ cd $CROSS_GCC_TMP
$ ln -s libgcc.a lib/gcc/i686-none-linux-gnu/4.7.2/libgcc_eh.a

安装内核头文件

安装内核头文件:

$ cd /vita/build
$ tar xvf ../source/linux-3.7.4.tar.xz
$ cd linux-3.7.4
$ make mrproper # 清理内核
$ make ARCH=i386 headers_check # 检查
$ make ARCH=i386 INSTALL_HDR_PATH=$SYSROOT/usr/ headers_install # 安装

编译目标系统的C库

编译目标系统的C库

$ sudo apt-get install gawk # 安装gawk
$ cd /vita/build
$ tar xvf ../source/glibc-2.15.tar.xz
$ cd glibc-2.15
$ patch -p1 < ../../source/glibc-2.15-cpuid.patch # 打补丁
$ patch -p1 < ../../source/glibc-2.15-s_frexp.patch # 打补丁
$ cd ..
$ mkdir glibc-build
$ cd glibc-build
$ ../glibc-2.15/configure \
    --prefix=/usr \
    --host=$TARGET \ # 交叉编译
    --enable-kernel=3.7.4 \ 限定版本
    --enable-add-ons \
    --with-headers=$SYSROOT/usr/include \
    libc_cv_forced_unwind=yes libc_cv_c_cleanup=yes\
    libc_cv_ctors_header=yes
$ make
$ make install_root=$SYSROOT install

glibc安装的主要文件:

  1. C库,glibc除了将最基本、最常用的函数封装在libc中外,又将功能相近的一些函数封装到一些子库里面。glib除了安装库文件就本身外,还建立了符号链接,包括:动态链接时的共享库符号链接,其命名格式一般为:libLIBRARY_NAME.so.MAJOR_REVISON_VERSION;开发时的共享库的符号链接,其命名格式一般为:libLIBRARY_NAME.so。glib将运行时使用的库安装在$SYSROOT/lib目录下,其中包括共享库文件本身及动态俩节气需要的符号链接;将开发时使用的库安装在$SYSROOT/usr/lib目录下,包括开发时需要的符号链接及静态库等。
  2. 动态链接器,glibc亦提供了加载共享库的工具:动态加载器,安装在$SYSROOT/lib目录下。
  3. 头文件,安装在$SYSROOT/usr/include。
  4. Glibc也提供了一些可执行的编译工具,这类工具一般安装在sbin、usr/bin、usr/sbin目录下。
  5. 启动文件,glibc提供了启动文件,包括ctr1.o、crti.o、crtn.o等。这类文件在编译链接时将被链接器链接到最后的可执行文件中,glibc将其安装在$SYSROOT/usr/lib目录下。

构建完整的交叉编译器

$ cd /vita/build/gcc-build
$ rm -rf *
$ ../gcc-4.7.2/configure \
    --prefix=$CROSS_TOOL --target=$TARGET \
    --with-sysroot=$SYSROOT \
    --with-mpfr-include=/vita/build/gcc-4.7.2/mpfr/src \
    --with-mpfr-lib=/vita/build/gcc-build/mpfr/src/.libs \
    --enable-languates=c,c++ --enable-threads=posix
$ make
$ make install

安装的主要内容如下:

  1. 驱动程序,gcc安装的最主要的时交叉编译器的驱动程序。
  2. 目标系统的库和头文件,gcc中也包含了一些用于目标系统的运行时库及头文件,它们安装在$CROSS_OOL/i686-none-linux-gnu目录下。在该目录下,子目录lib存放包括目标系统的运行时库以及共目标系统编译程序使用的静态库,子目录下包含开发目标系统上的程序需要的c++头文件。
  3. helper progrem,gcc仅仅是一个驱动程序,它将调用具体的程序完成具体的任务,这些程序被gcc安装在libexec目录下。
  4. freestanding实现文件,在lib目录下的头文件即为freestanding implementation实现标准要求的头文件。
  5. 启动文件,与C++相关的启动文件在gcc中,包括crtbegin.o、crtend.o等。

无论是C库,还是GCC都格子安装了头文件、运行库,GCC还安装了一些内部使用的可执行程序。那么在编译程序时,GCC时怎么找到这些文件的内?答案就是GCC内部定义的两个环境变量LIBRARY_PATH和COMPILER_PATH。

定义工具链相关的环境变量

GNU Make 使用了一些隐式的预定义变量,并且这些变量都有对应的默认值。这些隐式的预定义变量可以通过环境变量覆盖,或者在makefile中显示重新定义。为了避免在编译每一个软件包时,都需要显示指定使用我们构建的交叉工具链,我们在环境变量中定义编译过程使用的相关变量。

echo '
export CC="$TARGET-gcc"
export CXX="$TARGET-g++"
export AR="$TARGET-ar"
export AS="$TARGET-as"
export RANLIB="$TARGET-ranlib"
export LD="$TARGET-ld"
export STRIP="$TARGET-strip"
export DESTDIR=$SYSROOT # 后面安装编译程序时,通过给make传递变量DESTDIR指定make将它们安装到目标系统的根文件系统下。
' >> ~/.bashrc

如果需要重新构建交叉编译工具链,在构建前,要注释掉这一节的变量定义。

封装“交叉” pkg-config

在GNU中大部分的软件都使用Autoconf配置,Autoconf通常借助工具pkg-config去获取将要编译的程序依赖的共享库的一些信息,比如库的头文件存在在哪个目录,共享库存放在哪个目录下以及链接哪些共享库等,我们将其称为库的辕信息。通常,这些信息都被保存在一个以软件包的名称命名,并以“.pc”作为扩展模的文件中。二pkg-config会到特定的目录下寻找这些pc文件,一般而言,其首先搜索环境变量PKG_CONFIG_PATH指定的目录,然后搜索默认路径,一般是/usr/lib/pkgconfig、/usr/share/pkgconfig、/usr/local/lib/pkgconfig等。显然,使用PKG_CONFIG_PATH不能满足我们的要求,而另外一个环境变量PKG_CONFIG_LIBDIR,一旦设置了PKG_CONFIG_LIBDIR,其将取代pkg-config默认的搜索路径。因此,在交叉编译时,这两个变量的设置如下:

$ echo '
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR=$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig
' >> ~/.bashrc

除了pkg-config寻找pc文件的搜索路径需要调整外,从pc文件中获取的cflags和libs也需要追加sysroot作为前缀。因此,这里我们包装以下host系统的pkg-config,将为交差百衲衣定制的pkg-config放在$SYSROOT/bin下。

#!/bin/bash
HOST_PKG_CFG=/usr/bin/pkg-config

if [ ! $SYSROOT ]; then
    echo "please make sure you are in cross-compile environment!"
    exit 1
fi

$HOST_PKG_CFG --exists $*
if [ $? -ne 0 ]; then
    exit 1
fi

if $HOST_PKG_CFG $* | sed -e "s/-I/-I\/vita\/sysroot/g;s/-L/-L\/vita\/sysroot/g"; then
    exit 0
else
    exit 1
fi

如果需要重新构建交叉编译工具链,在构建之前,也需要注释掉此处的变量定义以及脚本。

关于使用 libtool 链接库的讨论

GNU 中的大部分软件包都使用libtool处理库的链接。通常,大部分的软件在包发布时都以及包含了libtool所需的脚本工具等。但是如果一旦准备使用autoconf、automake重新生成编译脚本,且这些脚本中包含了libtool提供的M4宏,则需要安装libtool。可以使用如下命令安装:

sudo apt-get install libtool

在交叉编译环境中使用libtool处理库的链接时,一人还有个不大不小的问题,如同pkg-config的麻烦一样,我们使用如下命令将la文件删除:

find $SYSROOT -name "*.la" -exec rm -rf '{}' \;

删除库的la文件后,链接相应的库时不再使用libtool去寻找库的文章,而是依赖链接器去寻找库的位置。

启动代码

C语言程序的入口函数是mian,这是因为启动代码的存在。在hosted environment下,应用程序运行在操作系统之上,程序启动前和退出前需要进行一些初始化和善后工作,而这些工作与hosted environment密切相关,并且是公共的,不属于应用程序范畴的事情,应用程序员无需关心。更重要的一点是,有些初始化动作需要在main函数运行前完成。有些操作不能使用C语言完成的,必须使用汇编指令。于是编译器和C库将它们抽取出来,放在了公共的代码中。

这些公共代码被称为启动代码,其实不只是程序启动时,也包括在程序推出时执行的一些代码,我们统称它们为启动代码,并将启动代码所在的文件称为启动文件。对于C语言来说,Glibc提供启动文件。显然,对于C++语言来说,因为启动代码是和语言密切相关的,所以其启动代码不再C库中,而由gcc提供。这些启动文件以"crt"开图、以“.o”结尾

results matching ""

    No results matching ""