Why do I need a Cross Compiler

来自osdev
跳到导航 跳到搜索
注意: 本页面是特定于GCC的。 如果你使用另一个编译器,你应该研究一下该编译器通常是如何进行交叉编译的,并且应该这样做。 GCC与其本机目标系统紧密绑定,许多其它编译器则没有。 有些编译器甚至没有本机目标,它们始终是交叉编译器。

‘’除非‘’你是在自己的操作系统上开发的,你需要使用交叉编译器。 编译器「必须」知道正确的 目标平台 (CPU,操作系统),否则会遇到麻烦。 如果你通过一系列选项来配置编译系统,你也许可以使用系统附带的编译器,但这会产生很多完全不必要的问题。

可以通过调用以下命令来询问编译器当前使用的目标平台是什么:

 gcc -dumpmachine

如果你在64位Linux上进行开发,那么你将获得诸如 “x86_64-unknown-linux-gnu” 之类的响应。 这意味着编译器认为它正在为Linux创建代码。 如果你使用这个GCC来构建你的内核,它将使用你的系统库、头文件、LINUX libgcc,并且它会做出很多有问题的LINUX环境假设。 如果你使用像i686-elf-gcc这样的 交叉编译器,那么你会得到一个响应,比如 “i686-elf”,这意味着编译器知道它在做别的事情,你可以轻松正确地避免很多问题。

如何构建交叉编译器

正文: GCC Cross Compiler

这很容易,需要花一些时间来 构建一个针对你的操作系统的交叉编译器。 在速度较慢的计算机上构建它可能需要一段时间,但你只需要做一次,你就可以节省所有的时间,否则你的时间就会花在“修复”你会遇到的完全不必要的问题上。 稍后,当你开始为操作系统构建用户空间时,有必要创建一个操作系统特定工具链来绝对控制编译器并轻松编译用户空间程序。

过渡到交叉编译器

也许直到现在你还没有使用交叉编译器,在这种情况下,你可能会做很多错误的事情。 不幸的是,许多内核教程建议针对传统编译器传递某些选项,回避这些问题,并以可能会造成很多麻烦的方式进行操作。 本节记录了一些你应该注意的事情。 请仔细阅读本节,如果你看到其他人使用了麻烦的选项,请向他们指出。

链接到你的编译器而不是ld

你不应该直接调用ld。 你的交叉编译器可以作为链接器工作,并使用它作为链接器,以便在链接阶段进行控制。 这些控制包括将-lgcc展开为只有编译器知道的libgcc的完整路径。 如果在编译过程中出现奇怪的错误,请使用交叉编译器进行链接,它可能会消失。 如果确实需要ld,请确保使用交叉链接器(i686 elf ld)而不是系统链接器。

使用交叉工具

当你构建你的交叉binutils时,你会得到很多有用的程序。 例如,你可以获得i686 elf readelf、i686 elf as、i686 elf objdump、i686 elf objcopy等等。 这些程序了解你的操作系统,并正确处理所有事情。 你可以使用本地操作系统附带的一些程序 (readelf,objcopy,objdump),如果他们知道你的操作系统的文件格式, 但一般来说,最好使用交叉工具。 如果你的操作系统平台是i686-ELF,这些工具的前缀都是“i686-ELF-”。

你应该使用的编译器选项

你需要向编译器传递一些特殊选项,告诉它并没有在构建用户空间程序。

-ffreestanding

这很重要,因为它可以让编译器知道它正在构建内核而不是用户空间程序。 GCC的文档中说,你需要在独立模式(freestanding mode)下自行实现memset、memcpy、memcmp和memmove功能。

-mno-red-zone(仅x86_64)

你需要在x86_64上传递这个参数,否则中断会损坏堆栈。 由于red zone是x86_64 ABI特性,这意味着信号发生在堆栈下128字节的位置时会发生问题。 允许使用少于该内存量的函数不递增堆栈指针。 这会导致内核中的CPU中断将损坏堆栈。 请确保对所有x86_64内核代码启用此功能。

-fno-exceptions, -fno-rtti (C++)

明智的做法是禁用在内核中无法开箱即用的C功能。 你需要向内核提供一个C++支持库(除了LIGBCC),以使所有C++特性都能工作。 如果你不使用这些C++特性,通过这些选项应该就足够了。

你应该使用链接的选项

这些选项只有在链接(而不是编译)时才有意义,你应该使用它们。 在链接时也应该传递编译选项,因为有些编译选项(比如-mno-red-zone控制ABI,这在链接时也需要知道)。

-nostdlib (与-nostartfiles -nodefaultlibs 两者相同)

-nostdlib选项与传递-nostartfiles-nodefaultlibs选项相同。 你不希望内核中包含启动文件(crt0.o、crti.o、crtn.o),因为它们只用于用户空间程序。 你不需要默认的库,例如libc,因为用户空间版本不适合内核使用。 你应该只需要-nostlib选项参数,因为它与同时使用后两个参数选项功能相同。

-lgcc

当你传递选项-nodefaultlibs (由-nostdlib隐含) 时,你禁用了重要的 libgcc 库。 编译器需要这个库来完成许多自己无法完成的操作,或者更有效地将这些操作放入共享函数中。 你必须在所有其它目标文件和库之后,在链接行的末尾传递此库,否则链接器不会使用它,你会得到奇怪的链接器错误。 这是由于经典的静态链接模式下,其中静态库中的目标文件仅在被先前的目标文件使用时才被拉入。 与libgcc的链接必须位于所有可能使用它的目标文件之后。

不应传递给编译器的选项

在构建内核时,你通常不应该将许多选项传递给交叉编译器。 不幸的是,很多内核教程建议你使用这些。 请不要在不了解为什么需要的情况下使用大量编译器选项,也不要建议其他人使用它们。 通常,这些选项被那些不使用交叉编译器的人,拿来掩盖其它问题。

-m32,-m64(编译器)

如果你构建了像i686-ELF-GCC这样的交叉编译器,那么你不需要告诉它生成32位可执行文件。 同样,你不需要将-m64选项提供给x86_64-elf-gcc。 这将使makefile变得更简单,因为你只需选择正确的编译器,一切都会正常工作。 你可以使用x86_64-ELF-GCC构建32位内核,但是单独构建两个交叉编译器并分别使用它们要容易得多。 此外,为目标的每个CPU使用交叉编译器将使移植第三方软件变得容易,而无需通过-m32选项欺骗它们。

-melf_i386, -melf_x86_64 (链接器)

由于上面与-m32和-m64相同的原因,你不需要使用这些参数。 此外,这些选项适用于ld,你不应该首先直接调用ld,而应该与交叉编译器链接。

-32,-64(汇编程序)

交叉汇编程序(i686-elf-as)默认使用你在构建binutil时指定的平台,因此你不需要在这里重复选择。 你可以将交叉编译器用作汇编器,但是直接调用汇编器也是可以的。

-nostdinc

你不应该传递此选项,因为它会禁用标准头包含目录。 但是,你会确实希望使用这些标头,因为它们包含许多有用的声明。 交叉编译器附带了一系列有用的头文件,比如stddef.h、 stdint.h、 stdarg.h、 还有其它更多头文件。

如果你不使用交叉编译器,你将获得不适合你的操作系统的主机平台(如Linux)的头文件。 因此,大多数不使用交叉编译器的人都使用此选项,然后必须自己重新实现stddef.h,stdint.h,stdarg.h等等头文件。 人们经常错误地实现这些文件,实现stdarg.h等功能还是需要一些编译器深入理解的

-fno-builtin

-ffreestanding 隐含了此选项,没有理由自己设置该选项。 编译器默认为-fbuiltin以启用内置函数,但-fno-builtin将禁用它们。 内置意味着编译器了解标准功能并可以优化它们的使用。 如果编译器看到一个名为 “strlen” 的函数,则通常假定它是C标准 “strlen” 函数,并且能够在编译时将表达式strlen (“foo”) 直接优化为3,而不是调用该函数。 如果你正在创建一些非常非标准的环境,其中常见的C函数没有它们通常的语义,那么这个选项很有价值。 可以在-ffreestanding之后使用-fbuiltin再次启用内置,但这可能会导致以后出现令人惊讶的问题,比如calloc(malloc+memset)的实现被优化为对calloc本身的调用。

-fno-stack-protector

Stack Smashing Protector是一种功能,它在所选函数的堆栈上存储一个随机值,并在返回时验证该值是否完整。 这在统计上可以防止堆栈缓冲区溢出覆盖堆栈上的返回指针,从而破坏控制流。 此功能要求攻击者能正确猜测一个32位值 (32位系统) 或64位值 (64位系统),使攻击者无法利用此类故障,。 此安全功能需要运行时支持。 来自许多操作系统供应商的编译器通过将-fstack-protector设为默认设置来启用此功能。 如果没有运行时支持,攻击者就会能破坏不使用交叉编译器的内核。 交叉编译器(如*-elf目标)在默认情况下禁用了堆栈保护器,最好不要自己再显式禁用它。 因为当你向内核(和用户空间)添加对栈保护的支持时,你可能希望再将缺省值更改为启用它,这将使内核能自动使用它。

在没有交叉编译器的情况下出现的问题

要使用系统gcc构建内核,需要克服很多问题。 如果使用交叉编译器,则不需要处理这些问题。

更复杂的编译命令

编译器假定它是针对本地系统的,因此需要很多选项才能使其正常工作。 用于在没有交叉编译器的情况下编译内核的精简命令序列可能如下所示:

 as -32 boot.s -o boot.o
 gcc -m32 kernel.c -o kernel.o -ffreestanding -nostdinc
 gcc -m32 my-libgcc-reimplemenation.c -o my-libgcc-reimplemenation.o -ffreestanding
 gcc -m32 -T link.ld boot.o kernel.o my-libgcc-reimplemenation.o -o kernel.bin -nostdlib -ffreestanding

实际上,一般情况更糟。 人们倾向于添加更多有问题或多余的选项。 而使用真正的交叉编译器,命令序列可能如下所示:

 i686-elf-as boot.s -o boot.o
 i686-elf-gcc kernel.c -o kernel.o -ffreestanding
 i686-elf-gcc -T link.ld boot.o kernel.o -o kernel.bin -nostdlib -ffreestanding -lgcc

重新实现libgcc

构建内核时不能使用主机libgcc。 上次我检查过Linuxlibgcc有一些令人讨厌的依赖项。 比如常见一种情况,新手遇到在32位系统上进行64位整数除法,但是编译器在许多情况下可能会生成此类调用。 当你本来应该使用真实的东西时,你经常会重写libgcc

重写freestanding头(通常不正确)

如果你不通过-nostdinc你得到目标系统头 (这是你的本地系统,如果不使用交叉编译器),这将导致很多问题在非交叉编译器的情况下。 最终,你将重写标准的独立头,例如stdarg.h、 stddef.h、 stdint.h、 还有更多。 不幸的是,如上所述,这些头文件需要一点编译器魔力才能恰到好处。 如果你使用交叉编译器,那么所有这些独立的标头都可以开箱即用。

复杂的用户空间程序编译

你需要将更多选项传递给为你的操作系统构建程序的命令行。 你需要一个-Ipath/ 到 /myos/include 和 -Lpath/ 到 /myos/lib来使用C库,以及更多。 如果你建立了一个OS Specific Toolchain,你只需要

 i686-myos-gcc hello.c -o hello

将hello world程序交叉编译到你的操作系统。

不同编译器发布版破坏你的操作系统

当你的自制系统代码发布后,不是每一个人都在和你使用一样的gcc,意味着在其它操作系统(甚至是版本或编译器发行版不同)上的人要正确构建你的操作系统,可能会遇到麻烦。 如果你使用交叉编译器,那么每个人都在使用相同的编译器版本,并且关于主机系统的环境假设不会进入你的操作系统。

支持

你将更容易获得操作系统开发社区的支持。 正确使用交叉编译器表明你遵循了说明,与其它所有人处于同一技术层,并且你的本地系统编译器没有造成麻烦。

等等

随着项目规模的增长,在没有真正的交叉编译器的情况下维护操作系统变得更加复杂。 即使你的ABI很像Linux,你的操作系统也不是Linux。 如果没有交叉编译器,移植第三方软件几乎是不可能的。 如果你设置了一个真正的 操作系统特定的工具链 和操作系统的 sysroot,则只需将-host=i686-myos提供给./configure即可编译软件。 使用交叉编译器,你可以以标准方式移植软件

背景信息

交叉编译的想法从何而来?

对于GNU软件,由于大部分都有从一个UNIX实现移植到另一个UNIX实现的悠久历史,他们提出了一套简单的规则: 你有一台构建机、一台主机和一台目标机。

交叉编译的基础是什么?

“构建”机就是编译软件的机器。 正在编译的该软件可以编译为在其它类型的计算机上运行。 例如,你可能正在基于x86的机器上构建,并希望该软件在基于SPARC的机器上运行。 构建机是隐式的,通常会由软件的配置脚本自动检测。 它唯一真正的目的是,如果正在编译的软件选择将用于配置它的configure参数保留在构建的软件包中的某个位置,则分发该软件包的人将知道该软件包是在什么机器上构建的。 如果构建机在构建该软件时存在某些问题,则可以使用构建机的名称将包配置为使用变通方法。

“主机”是软件必须在其上运行的机器。因此,在前面的示例中,“构建”机是一台i686-elf-yourBuildOS机器,主机是一台sparc32-elf-unix4。

在这个例子中,你必须有一个sparc32-elf-unix4交叉编译器,它可以在i686-elf-yourBuildOs的机器上运行,并编译出以你的主机为目标的结果。 交叉编译器通常以其目标主机命名,而不是以其运行的主机命名,因此通过查看编译器的名称,通常可以知道它以哪台机器为目标。

“目标”只在编译用于构建其它软件的软件时才重要。 即在编译获得编译器、链接器、汇编程序等时。 他们需要被告知自己将针对哪类机器。 当编译视频播放器或其它非构建类软件时,“目标”并不重要。 换句话说,“目标” 用于告诉正在编译的编译器或其它开发软件,即正在编译的编译器应该以什么为目标。

对于大多数软件,比如文本编辑器,如果你正在编译它,你只需要指定主机(通常甚至不需要指定主机)。 指定主机会导致使用构建机上针对该主机的交叉编译器构建软件。 这样,由于该软件是使用该主机的目标交叉编译器编译的,因此该软件将能够在该主机上运行,即使它是在 (可能) 不同的计算机上构建的。

一个具体事物的例子

你的发行版附带了一个面向yourMachineArch-distributionNativeExecFormat-distribution的编译器 并且在一台yourMachineArch-distributionNativeExecFormat-distribution机器上运行。 它是在其它机器上构建的(可能是在m68k-macho-osx或其它类似的好用机器上构建的),带有一个针对yourMachineArch-distributionNativeExecFormat-distribution的交叉编译器,在构建过程中,它自己的“目标”设置为yourMachineArch-distributionNativeExecFormat-distribution, 因此它现在既可以在上运行,又可以输出yourMachineArch-distributionNativeExecFormat-distribution的可执行文件

交叉编译器对于完全明确地针对特定机器很有用。 当你开始为一台独立于本机发行版的股票编译器的机器开发时,你将需要一个交叉编译器。 实际上,从你第一次尝试生成完全独立的可执行文件(即,当你在做OSDev时)起,你就已经开始尝试建立了一台与你的本机不同的机器。

你的本机编译器针对yourMachineArch-distributionNativeExecFormat-distribution。 但是你想把yourOsTargetArch-yourChosenFormat-none定为目标;其中,“none”作为操作系统名称表示没有系统库的机器。 编译内核时,你希望使用不知道目标计算机的任何系统库的编译器进行编译。 内核必须完全独立。 内核所需的一切都必须在其源代码树中,这样就不需要隐式的系统库(或者在你不知情的情况下链接进来)。

稍后,当你有了一个用户空间和一组可供程序链接的系统库时,你将构建一个目标为:yourOsTargetArch-yourChosenFormat-yourOs的编译器,同时保留面向yourOsTargetArch-yourChosenFormat-none的编译器。 两者之间的区别在于,“none” OS目标编译器在其/lib目录中没有任何库,并且没有可链接的系统库。 因此,使用这种“裸机”目标编译器编译的任何软件都不可能有未知的依赖项。 以“yourOS”为目标的交叉编译器是将你自己的系统库(在其/lib目录中)放在其中的地方,以便在构建软件时,它会将它们链接到那些系统库中,从而将它们链接到你的内核API中。 你将继续使用 “bare bones” 目标编译器来构建内核, 无论何时,只要你在开发机器上构建程序,并希望在内核的目标机器上运行, 你可以使用yourOsTargetArch-yourChosenFormat-yourOs编译器,它有一个/include和/lib目录,其中充满了你自己操作系统的本机系统包含和库。

稍后,当你确信可以从内核中开发和使用内核时,你将希望停止在单独的 “开发” 机器上构建所有程序。 更具体地说,在这一点上,你希望你的“构建”机器与测试机器相同,这样你就可以从操作系统内部构建在操作系统上运行的程序。 此时,是执行最后一步的时候了,在你单独的“开发”机器上,构建一个将在你的操作系统(host = yourOsTargetArch-yourChosenFormat-yourOs)上运行的编译器,并针对你的操作系统(target=yourOsTargetArch-yourChosenFormat-yourOs)。 这个交叉编译器,即GCC_Canadian_Cross,将允许你在自己的操作系统的用户空间上运行时本地编译其它程序。 它本质上是一个“发行版原生编译器”,就像发行版附带的编译器一样。 从这时起,就有了一个在你的操作系统上运行并面向你的操作系统的编译器,你基本上可以更自由地移植程序,并允许人们在运行你的操作系统时执行同样的操作(假设你将本地编译器与你的操作系统一起打包)。

我什么时候不需要交叉编译器?

如果你创建了一个真正的操作系统,并设法将gcc移植到它,那么gcc将生成与i686-myos-gcc完全相同的代码。 这意味着你不需要在你自己的操作系统上进行交叉编译,因为那里的GCC已经在做正确的事情了。 这就是linux内核使用Linux gcc而不是Linux交叉编译器构建的原因。

一个具体的例子

  • 我的Ubuntu机器上的gcc -v* 使用结果:
gravaera@gravaera-laptop:/void/cygwin/osdev/zbz$ gcc -v
Using built-in specs.
Target: i486-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.4.3-4ubuntu5' \
--with-bugurl=file:///usr/share/doc/gcc-4.4/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ \
--prefix=/usr --enable-shared --enable-multiarch --enable-linker-build-id --with-system-zlib \
--libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.4 \
--program-suffix=-4.4 --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-plugin --enable-objc-gc \
--enable-targets=all --disable-werror --with-arch-32=i486 --with-tune=generic --enable-checking=release \
--build=i486-linux-gnu --host=i486-linux-gnu --target=i486-linux-gnu
Thread model: posix
gcc version 4.4.3 (Ubuntu 4.4.3-4ubuntu5)

有人构建了在我的机器上运行的编译器,他在和我一样的机器上构建了它,一台‘i486-linux-gnu’(构建机),并且打算让它在像他/我这样的机器上运行: 同样是i486-linux-gnu(主机),他的意思是这个编译器是他为我构建,发出针对我的机器的可执行文件,这样当我编译程序时,结果程序将能够在i486-linux-gnu主机上运行。 因此,他使编译器目标平台为i486-linux-gnu。

另见

文章