深入交叉工具链[ING]

Published: 2022年07月24日

In Misc.

之前俺觉得交叉编译工具链能用就行,但几个月前被一个环境卡了快一周,无论如何构件的toolchain都无法使用,于是深入了解了下细节,之后忙着干其他事也没记,接下来又是两周的HVV,趁刚立了FLAG先记一些,以后慢慢记,现在是草稿状态,不要看!!!!

前置知识

兼容性

软件要在不同地方运行涉及到兼容性,这分为两种:

  1. API不兼容:从源码中能看出来,定义就不同,在linux下常见的是POSIX标准
  2. ABI不兼容:即使源码一样,也可能由于编译器/编译选项导致不同(目标指令集,对齐策略,调用约定,字节序,类型大小等),在Linux下常见的是System V标准

注:可通过readelf -h查看已有程序的abi

共享库版本

对ABI有了规范后,不同版本的库可能仍然不兼容,毕竟总会出现增删改接口与功能的情况,因此还要有版本组织的规范,linux下库通常会遵循lib<name>.so.<major>.<minor>.<micro>的命名规范:

  1. major:主版本是大升级,不同主版本不兼容
  2. minor:次版本是增量升级,只添加新接口,因此同主版本的次版本是向低版本兼容的
  3. micro:修补版本用于修bug或改进,不会改变abi因此可通用

根据major不兼容,minor向低版本兼容定义,有了SO-NAME,它就是为库创建名为lib<name>.so.<major>的符号链接,使其指向当前文件系统里存在的该major最高minor版,安装新库时可用ldconfig自动实现更新,而程序中只需要记录major号于是链接器只关注major号。

再看minor的约定,若在高次版本编译的程序未使用新接口,实际上它是可以使用低次版本库的,而用到新接口时会由于链接出错直接终止运行。

符号版本

符号版本(sym ver)[3]是在glibc中常见的机制,如glibc的c库使用libc-<major>.<minor>[.micro].so这种命名方式,在其内部,当它的接口被修改时,它会为新接口赋予不同的版本号,在编译时只需要记下使用到的最高版本号,就能保证在运行时是否能够满足兼容并运行,而此时libc的so-name不变,都是libc.so.6(指glibc2.x版本):

  ~ readelf -s /lib/x86_64-linux-gnu/libc.so.6 |grep memcpy
  1197: 00000000000a0950   183 IFUNC   GLOBAL DEFAULT   15 memcpy@@GLIBC_2.14
  1199: 00000000000bbad0    44 FUNC    GLOBAL DEFAULT   15 memcpy@GLIBC_2.2.5
...

如上,GLIBC_2.2.5是第一版,而GLIBC_2.14就是之后修改过的接口的新版本,这里@@表示默认版本,如果要链接的程序没有显式指定(如用gcc的__asm__( ".symver memcpy,memcpy@GLIBC_2.2.5");指令指定)则默认链接到该版本,使用这种机制不仅实现了向后兼容而且可以实现重载。

参考

[0] Library Interface Versioning in Solaris and Linux -- David J. Brown and Karl Runge (2000)

[1] 程序员的自我修养:链接、装载与库 -- 俞甲子 石凡 潘爱民 (2015)

[2] All about symbol versioning -- MaskRay (2020)

编译过程

只关注C语言,就是预处理->编译->汇编->链接,C语言的运行要求是有栈和初始化的BSS即可,但通常使用gcc编译是在Hosted环境下,默认会使用gcc的运行时和c标准库,即通常开发使用main作为入口点而实际入口点为_start,且总会链接libc,这里的libc又会依赖kernel,接下来将分解这里面所设计到的组件。

GCC

尽管Clang现在很猛,但交叉编译通常还是使用gcc,它包含多个编译前端,如C/C++/Ada/Go等语言,它将这些语言的源码编译为汇编代码,这里依然只关注C语言,它含cc1用于预处理与编译,将c代码转换成汇编语言代码,直观的看它只做C到ASM的翻译,但在大多数使用场景它需要做更多事,如在入口前生成必要的初始化代码,为硬件不支持的指令提供相关模拟,使用标准库等,因此它本身是有些依赖的。 首先是libgcc,这是GCC自身所使用的库(它不是C库),它的存在是为了让GCC更方便的进行编译,如它提供一些运算的实现从而使C代码在不支持该运算的CPU上也能被模拟(如一些整数或浮点运算),它本身由C和汇编实现,并且依赖C库的部分函数(不过我们可以只写声明并不实现它,只有在编译时真正用到该功能才需要实现,在交叉编译极其受限的环境下通常只需要声明),GCC默认该库存在并在必要时(编译阶段)发出对该库的调用(包括内联),在链接时若该库不存在则可能需要手动实现相关功能,而使用--without-headers选项可使编译libgcc不使用任何头文件(C库),这可用于构建引导编译器。 接着是RunTime Files,它包括startfiles和endfiles来包裹我们自己的代码,实现一些初始化(参数环境变量传递,线程,全局构造调用等)与收尾(资源回收,atexit等)工作,例如可能存在如下的RT文件:

image.png

可使用-nostartfiles-nostdlib选项来禁止引入start files(后者还会禁止引入libc,可见链接选项)。 最后就是libc,gcc通常和glibc组合,但也可以与uClibc,musl等库组合,这是在编译时确定的,之后无法更改。

binutils

binutils包含很多工具,里面的每个都很常用,这里只关注编译过程用到的:

  1. as:汇编器,如gas,可将gcc生成的汇编代码编译为机器码
  2. ld:链接器,将as生成的目标对象链接生成最终的可执行程序
  3. ar:用于操作静态库(*.a文件)
  4. ranlib:用于构建静态库的符号表

不像gcc,这些工具功能就很直观了,它们本身就是交叉工具,可输出特定目标,并且没有其他依赖,可喜可贺!

Kernel

再回到gcc->libc的依赖,通常的libc(glibc/uClibc等)运行在操作系统之上的用户空间里,它的很多功能需要使用操作系统提供的能力(系统调用),因此它需要kernel的头文件,通常包含include/linux,include/asm-generic两个通用目录和一个架构相关目录include/asm-$ARCH,但是直接复制的头文件里有些定义仅用于kernel,最好使用净化后的头文件[0],kernel提供make kernel_headers目标可实现自动提取,但它是kernel 2.6.19引入的[1],后来的ct-ng和buildroot都依赖它,因此新版工具无法编译低于此版本的kernel,实在需要则可用老版本,如crosstool-ng 1.2版,或直接使用crosstool构建。

回到kernel,这里主要关注系统调用,系统调用是比较稳定的API,它基本不会修改,只会新增,若要丢弃也是先标记过时并在相当长的时间后删除,因此它向后兼容,于是通常我们使用不高于目标内核版本的头文件编译出的工具链是能直接在高版本上运行的,因此为了通用型应尽可能在低版本上编译,但是整个整个工具链的编译过程涉及到的组件间版本是有依赖性的,在现代系统上编译越老的版本越容易失败,因此需要找到一个平衡点。

上面提到为了保证能用,编译时使用的内核不能高于目标的内核版本,因此需要先知道目标kernel的版本号,可通过如下方式查看版本号:

cat /proc/version
hostnamectl # systemd的工具
uname -a 
dmesg | grep Linux
cat /proc/sys/kernel/osrelease

但是有的目标去掉了版本信息,就无法通过这些方法获取,此时可它支持的特性推测,如通过新引入的特性确定,如proc里有fdinfo,它从2.6.22引入,cgroup 从2.6.24引入,因此可推断版本在它之后,可在LinuxVersions查看各版本引入的新功能。

注:1.还有个不同,旧版版本不存在x86目录,需要指定为i386或x86_64

2.通常没必要编译内核,只需要有内核头文件即可,若要编译内核需要匹配的gcc

LIBC

在交叉编译中常见的是uClibc/glibc/musl三个库,它们都是运行在kernel之上,还有如newlib可以直接在bare metal上运行,这里主要关注glibc,上面已经提到glibc用符号版本实现了向后兼容[2],通常旧程序可以使用新的glibc,反之若使用新接口则会出错(有hack的操作但[6]是不建议),glibc需要和gcc版本匹配,若还需要编译kernel则三者版本都要匹配,可根据年代来确定三者对应关系,年代相近的基本能用,如先确定kernel的发布时间,再看同期的gccglibc。glibc还支持制定kernel的兼容性,它可通过--enable-kernel=VERSION选项指定兼容的kernel版本[7]。

注: 1.没搞懂原因但被坑了很久,就是目标系统使用的是什么C库,工具链里也必须使用同样的C库,如目标是uClibc那么工具链也必须是uClibc,即使要构建静态链接的文件也必须如此,否则会出现段错误!

2.尽管uClibc和glibc是API兼容,但并不是ABI兼容的

参考

[0] Linux内核头文件 -- 别再闹了 (2020)

[1] Exporting kernel headers for use by userspace -- David Woodhouse

[2] How the GNU C Library handles backward compatibility -- DJ Delorie (2019)

[3] Linux and glibc API changes

[4] uClibc today: Still makes sense -- Alexey Brodkin (2017)

[5] Operating Systems And Applications For Embedded Systems -- Mariusz Naumowicz (2018)

[6] Glibc version header generator -- wheybags (2017)

[7] Glibc Binutils GCC 配置选项简介 -- 金步国 (2007)

构建交叉工具链

直接使用工具构建交叉编译环境

一般情况下设备运行环境中是不会存在开发工具的,而且一般也不存在包管理工具,因此需要在其他环境中编译必要的软件并上传到设备中,由于ABI(指令集/系统调用/大小端/文件格式)不同,需要用到交叉编译,一般需要两步,编译交叉编译器,再用交叉编译器编译得到目标程序,其中涉及三个概念:

  1. --build:指编译交叉编译器时使用的平台,不指定一般工具也能推测出
  2. --host:指编译出的交叉编译器需要运行的平台,不指定时和build一致
  3. --target:编译出的交叉编译器编译代码后生成的可执行文件运行的平台

如有一台超强的mips主机,但是它上面没有编译环境。有一台很菜的x86主机,它上面有gcc。现在编译个巨大无比的运行在arm平台上的程序,于是用--build x86 --host mips --target arm参数编译gcc,这样能生成一个mips版的gcc,它运行在mips主机上并能编译出在arm上执行的程序。当有了交叉编译器,只需要指定build和host参数即可。有一些工具能方便的构建交叉编译器,如buildroot和crosstool-ng。

buildroot

如它的名字,它是用于构建完整rootfs的,使用它可以很容易生成一个嵌入式linux系统环境,包括指定的内核镜像,同样也有针对该环境进行开发所需的交叉编译工具,在生成交叉编译工具时,而且它有glibc/uClibc-ng/musl三种库可选。它的使用方法如下:

  1. 下载最新稳定版的压缩包:https://buildroot.org/download.html
  2. 安装必要的工具软件:https://buildroot.org/downloads/manual/manual.html#requirement
  3. 使用make menuconfig生成配置文件,重点关注target optionstoolchain,前者选择架构,后者选择系统版本号和c库,此处选择的系统版本必须不大于设备系统版本
  4. 使用make编译,编译后结果在output目录,编译工具链在其host子目录中。

在使用它构建kernel时,需要先配置kernel的构建选项,可手动进入output/build里响应的linux源码目录,复制arch下的默认配置,或者使用make menuconfig生成配置。

crosstool-ng

相比于buildroot,它是更纯粹的交叉编译器生成工具,它可针对bare-metal或linux生成工具,并且支持glibc和uClibc-ng作为c库,不过它默认支持的系统版本较少,低版本需要使用git等方式手动配置,它的使用方法如下:

  1. 下载最新的代码:git clone https://github.com/crosstool-ng/crosstool-ng
  2. 安装必要的软件:apt-get install bison flex texinfo automake m4 libtool-bin libncurses-dev bison flex texinfo automake help2man,剩下的之后报错再补
  3. 进入目录运行./bootstrap,它会生成automake配置
  4. 执行./configure --prefix=/opt/crosstool-ng && make && make install生成并安装ct-ng
  5. 新建目录,在该目录下生成交叉编译配置:/opt/crosstool-ng/bin/ct-ng menuconfig,它的配置和buildroot类似,看菜单和对应的帮助即可
  6. 执行编译命令:/opt/crosstool-ng/bin/ct-ng build
  7. 之后即可指定x-tools下的程序为编译工具,对于使用动态链接的文件,需要把sysroot下的依赖库拷贝到目标系统。

深入分析构建过程

GNU三元组<CPU>-<MANUFACTURER>[-<KERNEL>]-<OS>

有时在menu里找不到可以直接编辑.config文件

/root/x-tools/x86_64-linux2_6_22-linux-gnu/bin

启用调试:

 [*] Debug crosstool-NG                                                                                                                                               x x  
  [ ]   Pause between every steps                                                                                                                                      x x  
  [*]   Save intermediate steps                                                                                                                                        x x  
  [*]     gzip saved states                                                                                                                                            x x  
  [*]   Interactive shell on failed commands   
root@bm:/home/bm/ctng# ct-ng list-steps
Available build steps, in order:
  - companion_tools_for_build
  - companion_libs_for_build
  - binutils_for_build
  - companion_tools_for_host
  - companion_libs_for_host
  - binutils_for_host
  - cc_core_pass_1
  - kernel_headers
  - libc_start_files
  - cc_core_pass_2
  - libc_main
  - cc_for_build
  - cc_for_host
  - libc_post_cc
  - companion_libs_for_target
  - binutils_for_target
  - debug
  - test_suite
  - finish

如isl下载失败,编辑.config修改下载网址为https://libisl.sourceforge.io/

参考

[0] How a toolchain is constructed

[1] http://www.eglibc.org/cgi-bin/viewvc.cgi/trunk/libc/EGLIBC.cross-building?view=markup

[2] https://preshing.com/20141119/how-to-build-a-gcc-cross-compiler/

[3] https://sourceware.org/legacy-ml/libc-alpha/2018-03/msg00131.html

[4] https://sourceware.org/legacy-ml/libc-alpha/2012-03/msg00237.html

[5] https://wiki.osdev.org/GCC_Cross-Compiler

[6] https://wiki.osdev.org/OS_Specific_Toolchain

[7] https://wiki.osdev.org/Hosted_GCC_Cross-Compiler

[8] https://sourceware.org/legacy-ml/crossgcc/2003-06/msg00170.html

其他已有工具链

https://elinux.org/Toolchains 介绍了很多构建工具链的工具

  1. 这里可下载老版uClibc工具链
  2. 可使用旧版系统的工具链,如使用ubuntu,先查看ubuntu与kernel版本对照获取对应的ubuntu版本,再安装,旧版通常需要换archive源,配置好源后就可以安装仓库里的工具链

为目标环境编译程序

构建通用分析工具

像GitHub的gdb binary项目,可以通过交叉编译工具构建一批兼容性强的分析工具,包括:

  1. gdbgdbserver,一般需要带python扩展,也就是python顺便也要来一份,方便使用如GEFpwndbg插件。
  2. 有些时候不好用gdbserver,如unix环境,需要编lldb
  3. 像ida在做伪代码调试时挺好用的,但是它的server是动态链接的而且libc版本还挺高,需要构建多版本的libc库。
  4. 很多环境里只有服务运行必须的工具,因此扔一个busybox很好用,所以也要储备着。
  5. 网络的netcat,tcpdump,sshd,隧道工具等,也是常用的,还有strace,ltrace等工具...

这些工具就是积累,用到了就把它保留好,把它们变成祖传的,它们的编译方法大同小异,这里随便找个例子,在分析产品时,想使用strace,它不自带因此需要自己编译,使用uname可知kernel为2.6.32:

bash-4.1# uname -a
Linux localhost2 2.6.32-00025-g841d072-dirty #1 SMP Mon Jul 20 17:51:26 EDT 2020 x86_64 x86_64 x86_64 GNU/Linux

创建交叉编译工具链,使用ct-ng menuconfig配置编译目标: image.png 配置操作系统,此处版本存在的话可以直接选择,否者可以指定从git的某个版本或本地的版本,这里指定内核头文件为2.6.33之前的版本(为了通用可以稍微选个小一点的版本): image.png 这里可以选择使用glibc或是uClibc及其版本: image.png 此处选择gcc的版本: image.png 编译普通程序只需指定Linux内核版本与运行目标平台,编译内核的话对gcc版本会有要求,配置好后使用ct-ng build编译即可,之后即可编译程序:

# 在ubuntu20上下载源码,指定工具链并编译
wget  https://github.com/strace/strace/releases/download/v5.13/strace-5.13.tar.xz
tar xf strace-5.13.tar.xz && cd strace-5.13
export PATH=$PATH:/home/bm/x-tools/x86_64-unknown-linux-uclibc/bin
./configure --host=x86_64-unknown-linux-uclibc CFLAGS=-std=c99 --prefix=/opt/strace
make && make install 

之后把编译好的二进制文件传入设备里即可,此处使用了默认的编译选项,即动态链接,因此还需要sysroot里的动态库,此处存放于/opt/lib目录下:

curl http://192.168.202.133:8000/strace > strace  # 传输strace
curl ....   > /opt/lib/...  # 传输必要的依赖
alias strace='LD_LIBRARY_PATH=/opt/lib /opt/strace/strace' 

一般应选择静态链接,除非静态链接比较困难。