点击上方蓝色“ Linux News搬运工 ”关注我们~
Linker limitations on 32-bit architectures
August 27, 2019
This article was contributed by Alexander E. Patrakov
程序要能运行,必须得先被编译出来。大家越来越认识到现代软件消耗的运行资源要比以前的软件大的多,有时甚至要求用户必须得升级计算机硬件。其实与此同时,编译过程中也需要占用更多资源了,所以负责编译发行Linux发行版的管理者就需要投资购买更快的CPU、更多内存。不过在32位系统架构中,对虚拟内存空间有限制,所以现在这些发行版在针对那些架构编译软件包的时候就碰到了各种问题。
如果只用32位地址,那么进程就只能访问不超过4GB内存空间。在某些体系架构上甚至能访问的内存空间更加小,例如MIPS如果不带EVA硬件(Enhanced Virtual Addressing)的话,就必须得把高2GB的虚拟空间专用于kernel和supervisor mode。这样在需要把编译目标文件链接生成比较大的程序或者library的时候,链接器ld有时候需要超过2GB的内存空间,就会失败。
这类问题是最近由Aurelien Jarno在一封发往多个Debian mailing list的邮件里面提出的。当然他并不是第一个碰到问题的人,此前在2012年在Gentoo forum就有一个关于webkit-gtk的帖子在抱怨了。我们来看一下这个例子,此外其他很多软件包也会有类似问题(Jarno提到firefox, Glasgow Haskell Compiler, Rust,和一些科学计算软件),就是进行32位编译的时候报错说虚拟地址空间不够。
在编译webkit-gitk的时候,首先会把C++源代码文件都分好组,拼接在一起,生成的文件被称为“unified sources”。先做这个步骤的好处是能减少编译时间。可以看WebKit开发者Michael Catanzaro的博客文章来详细了解:https://blogs.gnome.org/mcatanzaro/2018/02/17/on-compiling-webkit-now-twice-as-fast/
有意思的是,此前就有人建议采用这种方法,不过当时的目标是减少link阶段内存需求。
Unified source会用g++来编译生成目标代码文件,g++知道该调用哪个编译器(cc1plus)或者汇编器(as)。此阶段中最耗内存的程序是cc1plus,通常会消耗~500-800MB的RAM,在处理有些特别复杂的文件时可能会超过1.5GB。因此在只有32位物理地址空间的系统上,想要并行编译处理webkit-gtk从而加上-j2甚至-j3参数很有可能就会出错。换句话说,在真正的32位系统上,因为内存不够,我们甚至可能无法充分利用多进程的并行处理。不过好在无论是32位的x86还是MIPS,都有相应的CPU架构扩展(extension)来允许系统整体(注意这里不是针对某个单独进程)能使用超过4GB的物理内存。
Debian, Fedora,以及其他很多发行版(不过不包括Gentoo)都会在编译C/C++源文件的时候生成调试信息。这样在程序crash生成core dump文件的时候可以利用这些调试信息来分析出错原因,因为它们可以明确指出出错指令对应了源代码的哪一行,以及查看各个局部变量和全局变量在内存中的位置,怎么解析程序里面的各种数据结构。假如使用了-g编译器参数来生成调试信息,最终生成的目标文件可能会从几KB到10MB不等。例如WebDriverService.cpp.o就占用了2.1MB的磁盘空间,如果去除掉调试信息的话,就只需要208KB就够了。也就是说,这个文件里有90%的空间都是存放调试信息的。
编译器生成这些目标文件之后,会调用ld来把这些文件链接在一起生成最终可执行程序或者函数库。这个过程会把目标文件里的代码、数据、调试信息等都装进一个ELF文件里,并且修正互相引用的地址。在最终链接生成libwebkit2gtk-4.0.so.37的时候,会一下子把3GB的目标文件传递给ld。如果x86带有PAE(Physical Address Extension物理地址扩展)的话应该不会出错,不过也很临界了。我们可以在LDFLAGS里使用 -Wl,--stats 参数生成报告如下:
ld.gold: total space allocated by malloc: 626630656 bytes total bytes mapped for read: 3050048838 output file size: 1727812432 bytes
把前两行的数值加起来就看出为ld.gold总共分配了3.6GB的空间。这里大家肯定会对资源占用很有疑问。首先,这些内存(或者地址空间)都用来做什么了?其次,ld是否能再少用一点内存?
其实binutils里面实现了两个ld,分别是ld.bfd(旧版本,不过目前仍然是缺省版本)和ld.gold(新版本)。缺省来说这两个ld都会使用mmap()来把目标文件读进来作为输入。也就是说ld针对每一个目标文件会映射到它自己的一段虚拟地址空间上去,这样所有针对这段地址空间的内存操作都会被kernel通过page fault机制来重定向到文件操作。 这种编程模式非常方便,理论上来说也会减少物理内存的占用,因为kernel不用被迫长期占用这段物理内存,而是可以在需要的时候自行把这段缓存了目标文件部分内容的内存空间释放出来做其他用途,并且今后ld需要读这部分目标文件数据的时候又可以自动再次从文件读到物理内存来。这种方案的缺点就是必须要确保有足够的虚拟内存空间足够覆盖所有输入输出文件,否则mmap()操作就会失败。
ld.bfd和ld.gold两者都提供了命令行参数来帮助调整保留内存,不过其实只是有时候有用:
$ ld.bfd --help --no-keep-memory Use less memory and more disk I/O --reduce-memory-overheads Reduce memory overheads, possibly taking much longer --hash-size=<NUMBER> Set default hash table size close to <NUMBER> $ ld.gold --help --no-keep-files-mapped Release mapped files after each pass --no-map-whole-files Map only relevant file parts to memory (default) --no-mmap-output-file Do not map the output file for writing
当发行版维护者看到32位系统上出现编译错误的时候,如果用了上述参数中的某一项能解决问题,那么就会尽快提交一个patch给upstream来确保编译这个应用程序或库文件的时候缺省使用正确的参数。webkit-gtk的编译问题通常就是这么解决的。换句话说,所有容易解决的问题已经都解决了(all the low-hanging fruit is already collected,学个地道的英语说法——译者)。
对用户来说,比起没有安装包可用来说,能拿到一个不带调试信息的安装包,其实就已经很满意了。所以,发布者就被迫要把调试信息压缩一下,或者减少一些调试信息,甚至极端情况需要彻底拿掉调试信息。在Debian里面还是会有一些安装包在某些体系架构上是无法安装的,因为它们编译时会由于虚拟内存不足而导致编译失败。Jarno的结论是:“我们是时候要停止小修小补了,需要找一个真正的解决方案”。
有嵌入式系统开发经验的读者可能会问,为什么Debian会需要在计算能力很弱、资源有限的32位系统上编译安装包?不应该在更强大的amd64服务器上采用交叉编译(cross-compile)来生成安装包吗?确实,Yocto Project和Buildroot就是这么做的,他们就没有在哪怕一个体系架构上碰到编译带完整调试信息的webkit-gtk安装包的问题。不过,交叉编译就意味着无法执行所编译好的代码。有些安装包本来需要通过编译、运行一些小测试代码来检测系统配置,在交叉编译环境下就不能这么做了,所以只能完全依赖外部给出的配置信息,hard-code的信息,甚至做一些假设(通常都是按照悲观的情况做假设)。此外,维护者也就不能在编译过程中运行相应的test suite来做测试了。对Debian发布团队来说这些限制完全无法接受,所以他们就总是要求要进行本地编译,不能做交叉编译。相应的,虽然可以在QEMU这种模拟器里运行测试,但这个方案也是无法接受的,因为模拟器本身可能还会有各种问题。
Jarno建议利用Debian当前的"multiarch"支持能力。这是一个机制能同时安装针对多种架构的安装包。例如,一个系统上如果在运行amd64 kernel,那么i386和amd64的安装包都会被安装上去,所以这两种架构的程序都可以运行。同样的,32位和64位MIPS架构也做了同样的事情。这样一来,我们就可以生成运行在64位系统上、但是会生成32位程序的交叉编译器了。在gcc上这个很容易,只要创建一个wrapper来加上-m32开关就好。不过gcc并不是唯一的编译器,还有Haskell, Rust, OCaml等gcc等支持的语言,他们也需要按这个规则来提供编译器。
假如按照这个方案来走,那么如何得到所有这些编译器呢?也就是说,如果一个安装包在编译阶段需要占用非常非常多的内存,那么是应该用交叉编译生成,还是应该直接缺省提供呢?还有一个困难是关于RISC-V架构的:它的64位CPU并没有实现32位的指令,所以编译出来的32位程序没法保证能够执行。对Arm处理器来说也有类似的问题,有的arm64硬件上并没有实现32位指令。不过,这个阻力并不大,毕竟Debian还不支持32位的RISC-V系统,并且现在常用的arm64硬件还是支持32位指令的,可以直接拿来作为编译服务器。所以Ivo De Decker (Debian发布团队的一员)表示在这个方案可行之后,可以重新评估改变现行的编译策略。
此外,Sam Hartman(目前的Debian Project Leader)也表明,他并不觉得在32位系统上运行编译工具(包括编译器和链接器)是个好主意。当然,这个观点跟发布团队的观点并不一致,并且需要完成精确测量编译时间的测试报告。还有人提到一个解决方案就是利用distcc,不过最后讨论下来这个方案行不通,因为distcc的链接阶段仍然是本地完成的。
Luke Kenneth Casson Leighton也提出了对生态方面的担忧,因为这样就相当于是要迫使一些仍然能完美使用的硬件被淘汰进垃圾场了,仅仅因为编译工具里面使用了很浪费资源的算法。在Leighton看来,必须让编译工具这边修正行为,需要能支持比系统可用的虚拟内存空间大的多的文件,其实十几年前它本来就能做到这一点。
就目前来说,32位系统仍然用途广泛,也能运行通用操作系统。只有很少数的特别大的安装包会受到这个编译时资源不足问题的影响。不过终究有一天,我们会走到Hartman说的这一步:“如果没人做这件事,我们会失去所有32位架构”,至少对Debian是如此。
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
极度欢迎将文章分享到朋友圈
长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~