Why do I need a Cross Compiler
:注意:此页面特定于GCC。 如果您使用另一个编译器,您应该研究通常如何使用该编译器进行交叉编译,并以这种方式进行。 GCC与本机目标系统的绑定非常紧密,而许多其他编译器则不然。 有些编译器甚至没有本机目标,它们始终是交叉编译器。
您需要使用U交叉编译器|交叉编译器,除非您是在自己的操作系统上开发的。 编译器“必须”知道正确的 Target platform(CPU、操作系统),否则您将遇到麻烦。 如果您传递了许多选项以使其提交,那么您可能可以使用系统附带的编译器,但这将产生许多完全不必要的问题。
可以通过调用以下命令询问编译器当前使用的目标平台:
gcc-转储机器
如果您是在64位Linux上开发的,那么您将得到一个响应,如“x86_64-unknown-Linux-gnu”。这意味着编译器认为它正在为Linux创建代码。 如果您使用这个GCC来构建内核,它将使用您的系统库、头文件、Linuxlibgcc,并且会产生很多有问题的Linux假设。 如果您使用 Cross-Compiler,例如i686 elf GCC,那么您会得到一个响应,例如“i686 elf”,这意味着编译器知道它正在做其他事情,您可以轻松、正确地避免许多问题。
如何构建交叉编译器
针对您的操作系统的 build a Cross-Compiler非常简单,而且需要一些时间。 在速度较慢的计算机上构建它可能需要一段时间,但您只需要做一次,并且您可以节省所有时间,否则您将花费在“修复”您将遇到的完全不必要的问题上。 稍后,当您开始为操作系统构建用户空间时,值得创建 OS-Specific Toolchain以绝对控制编译器并轻松编译用户空间程序。
过渡到交叉编译器
也许到目前为止您还没有使用u交叉编译器|交叉编译器,在这种情况下,您可能会做很多错误的事情。 不幸的是,许多内核教程建议传递某些选项,并以一种可能导致很多麻烦的方式进行操作。 本节记录了您应该注意的一些事项。 请仔细阅读本节,如果您看到其他人使用了麻烦的选项,请向他们指出。
与编译器而不是ld链接
你不应该直接调用ld。 您的交叉编译器可以作为链接器工作,并使用它作为链接器,以便在链接阶段进行控制。 此控件包括将-lgcc扩展到只有编译器知道的libgcc的完整路径。 如果在编译过程中出现奇怪的错误,请使用交叉编译器进行链接,它可能会消失。 如果确实需要ld,请确保使用交叉链接器(i686 elf ld)而不是系统链接器。
使用交叉工具
当您构建交叉binutil时,会得到很多有用的程序。 例如,您可以获得i686 elf readelf、i686 elf as、i686 elf objdump、i686 elf objcopy等等。 这些程序了解您的操作系统并正确处理所有问题。 如果您的本地操作系统附带的一些程序(readelf、objcopy、objdump)知道您的操作系统的文件格式,您可以使用它们,但通常最好使用跨工具。 如果操作系统的平台是i686 elf,则这些工具都始终具有前缀“i686 elf-”。
应该传递给编译器的选项
您需要将一些特殊选项传递给编译器,以告知它没有构建用户空间程序。
-ffreestanding
这很重要,因为它让编译器知道它正在构建内核而不是用户空间程序。 GCC的文档中说,您需要在独立模式下自行实现memset、memcpy、memcmp和memmove功能。
-mno-red-zone (x86_64 only)
您需要在x86_64上传递此消息,否则中断将损坏堆栈。 红色区域是x86_64 ABI特性,这意味着信号发生在堆栈下128字节的位置。 允许使用少于该内存量的函数不增加堆栈指针。 这意味着内核中的CPU中断将损坏堆栈。 请确保对所有x86_64内核代码都通过启用。
-fno-exceptions, -fno-rtti (C++)
禁用内核中不工作的C++特性是明智的。 你需要向内核提供一个C++支持库(除了LIGBCC),以使所有C++特性都能工作。 如果不使用这些C++特性,则应该足以传递这些选项。
您应该链接到的选项
这些选项只有在链接(而不是编译)时才有意义,您应该使用它们。 链接时还应传递编译选项,因为某些编译选项(如-mno red zone控制ABI,这也需要在链接时知道)。
-nostdlib (same as both -nostartfiles -nodefaultlibs)
The -nostdlib option is the same as passing both the -nostartfiles -nodefaultlibs options. 您不希望启动文件(crt0.o、crti.o、crtn.o)位于内核中,因为它们只用于用户空间程序。 您不需要libc之类的默认库,因为用户空间版本不适合内核使用。 您应该只传递-nostlib,因为它与传递后两个选项相同。
-lgcc
You disable the important libgcc library when you pass -nodefaultlibs (implied by -nostdlib). The compiler needs this library for many operations that it cannot do itself or that is more efficient to put into a shared function. 在所有其他对象文件和库之后,必须在链接行的末尾传递此库,否则链接器将不使用它,并且会出现奇怪的链接器错误。 这是由于经典的静态链接模型造成的,在该模型中,只有在以前的对象文件使用静态库中的对象文件时,才会将其拉入。 与libgcc的链接必须位于所有可能使用它的对象文件之后。
不应传递给编译器的选项=
在构建内核时,有许多选项通常不应该传递给交叉编译器。 不幸的是,许多内核教程建议您使用这些。 请不要在不理解为什么需要某个选项的情况下传递该选项,也不要建议用户使用该选项。 通常,这些选项被那些不使用交叉编译器的人用来掩盖其他问题。
-m32, -m64 (compiler)
If you build a cross-compiler such as i686-elf-gcc, then you don't need to tell it to make a 32-bit executable. Likewise, you don't need to pass -m64 to x86_64-elf-gcc. This will make your Makefiles much simpler as you can simply select the correct compiler and things will work. You can use x86_64-elf-gcc to build a 32-bit kernel, but it's much easier to just build two cross-compilers and use them. In addition, using a cross-compiler for every CPU you target will make it easy to port third-party software without tricking them into passing -m32 as well.
-melf_i386, -melf_x86_64 (linker)
您不需要因为与-m32和-m64相同的原因通过这些。此外,这些选项是针对ld的,您首先不应该直接调用ld,而应该与交叉编译器链接。
-32, -64 (assembler)
交叉汇编程序(i686 elf as)默认为您在构建binutils时指定的平台,因此您无需在此处重复选择。 您可以将交叉编译器用作汇编程序,但直接调用汇编程序是可以的。
-nostdinc
您不应该传递此选项,因为它会禁用标准标头包含目录。但是,您确实希望使用这些头,因为它们包含许多有用的声明。交叉编译器附带了一系列有用的头文件,如stddef。h、 斯特丁。h、 斯塔格。h、 还有更多。
如果不使用交叉编译器,则会得到不适合操作系统的主机平台(如Linux)的标题。出于这个原因,大多数不使用交叉编译器的人使用这个选项,然后必须重新实现stddef。h、 斯特丁。h、 斯塔格。h和更多的人自己。人们经常错误地实现这些文件,因为您需要编译器魔法来实现诸如stdarg之类的功能。H
-fno-builtin
此选项由-f重新理解表示,您没有理由自己通过此选项。 编译器默认为-fbuiltin以启用内置,但-fno builtin将禁用它们。 内置意味着编译器了解标准特性并可以优化它们的使用。 如果编译器看到一个名为“strlen”的函数,它通常会假定它是C标准的“strlen”函数,并且能够在编译时将表达式strlen(“foo”)优化为3,而不是调用该函数。 如果您正在创建一些非常非标准的环境,其中常见的C函数没有它们通常的语义,那么这个选项很有价值。 在-f重新理解之后,可以使用-fbuiltin再次启用内置,但这可能会导致令人惊讶的问题,例如,将calloc(malloc+memset)的实现优化为调用calloc本身。
-fno-stack-protector
The Stack Smashing Protector is a feature that stores a random value on the stack of selected functions and verifies the value is intact upon return. 这在统计上防止堆栈缓冲区溢出覆盖堆栈上的返回指针,从而破坏控制流。 对手通常能够利用此类故障,此功能要求对手正确猜测32位值(32位系统)或64位值(64位系统)。此安全功能需要运行时支持。 许多操作系统供应商的编译器通过将-fstack-protector作为默认值来启用此功能。 这会破坏不使用交叉编译器的内核,如果它们没有运行时支持的话。 交叉编译器(如*-elf目标)在默认情况下禁用了堆栈保护器,您没有理由自己禁用它。 当您向内核(和用户空间)添加对它的支持时,您可能希望将默认设置更改为启用它,这将使内核自动使用它,因为您没有通过此选项。
没有交叉编译器时出现的问题
要使用系统gcc构建内核,需要克服很多问题。 You don't need to deal with these problems if you use a cross-compiler.
更复杂的编译命令
The compiler assumes it is targetting your local system, so you need a lot of options to make it behave. A trimmed down command sequence for compiling a kernel without a cross-compiler could look like this:
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
Actually, the average case is worse. People tend to add many more problematic or redundant options. With a real cross-compiler, the command sequence could look this this:
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
You cannot use the host libgcc when building a kernel. The Linux libgcc has some nasty dependencies last timed I checked. The common case newbies run into is 64-bit integer division on 32-bit systems, but the compiler may generate such calls in many cases. You will often end up rewriting libgcc when you should have been using the real thing in the first place.
Rewriting freestanding headers (often incorrectly)
If you don't pass -nostdinc you get the target system headers (which is your local system if not using a cross-compiler), and that will cause a lot of problems in the non-cross-compiler case. You will end up rewriting the standard freestanding headers such as stdarg.h, stddef.h, stdint.h. and more. Unfortunately, as mentioned above, these headers need a bit of compiler magic to get just right. If you use a cross-compiler, all these freestanding headers can be used out of the box with no effort.
复杂的用户空间程序编译
You need to pass even more options to the command lines that build programs for your operating systems. You need a -Ipath/to/myos/include and -Lpath/to/myos/lib to use the C library, and more. If you set up an OS Specific Toolchain, you just need
i686-myos-gcc hello.c -o hello
to cross-compile the hello world program to your operating system.
Compiler releases break your OS
Not everyone is using the same gcc as you are, which means that people on other operating systems (even versions, or compiler releases) will have trouble building your operating system correctly. If you use a cross-compiler, then everyone is using the same compiler version and assumptions about the host system won't make it into your operating system.
支持
You will have a much easier time getting support from the operating system development community. Properly using a cross-compiler shows that you have followed instructions and are at the same level as everyone else, and that your local system compiler isn't causing trouble.
等等
随着项目规模的增长,在没有真正的交叉编译器的情况下维护操作系统变得更加复杂。 即使你的ABI很像Linux,你的操作系统也不是Linux。 如果没有交叉编译器,移植第三方软件几乎是不可能的。 如果您设置了真正的OS-Specific Toolchain和sysroot操作系统,只需将--host=i686 myos提供给即可编译软件/配置。 使用交叉编译器,您可以以标准方式端口软件。
背景资料
交叉编译的想法从何而来?
对于GNU软件,由于大部分都有从一个UNIX实现移植到另一个UNIX实现的悠久历史,他们提出了一组简单的规则:您有一台构建机、一台主机和一台目标机。
交叉编译的基础是什么?
“构建”机器就是您在其上编译软件的机器。 正在编译的软件可能被编译为在其他类型的机器上运行。 请看,您可能正在基于x86的机器上构建,并希望该软件在基于SPARC的机器上运行。 生成机器是隐式的,通常由软件的配置脚本自动检测。 它唯一的真正用途是,如果正在编译的软件选择将用于配置它的configure参数保留在构建包中的某个位置,那么向其分发包的人将知道包构建在哪台机器上。 如果构建机在构建该软件时存在某些问题,则可以使用构建机的名称将包配置为使用变通方法。
“主机”机器是软件必须运行的机器。 因此,在前面的示例中,“构建”机器是一台i686 elf yourBuildOs机器,主机是一台sparc32-elf-unix4机器。
在本例中,您必须有一个sparc32-elf-unix4交叉编译器,它可以在i686 elf yourBuildOs机器上运行,并吐出(目标)主机。 交叉编译器通常以其目标主机命名,而不是以其运行的主机命名,因此通过查看编译器的名称,通常可以知道它目标的机器。
“目标”仅在编译用于构建其他软件的软件时起作用。 也就是说,在编译编译器、链接器、汇编程序等时。 他们需要被告知自己将瞄准哪台机器。 当编译电影播放器或其他非建筑软件时,“目标”并不重要。 换言之,“target”用于告诉编译器或其他正在编译的开发软件它的目标主机,即正在编译的编译器。
对于大多数软件,如文本编辑器,如果您正在编译它,您只需要指定主机(通常甚至不需要指定主机)。指定主机会导致使用生成计算机上的编译器生成软件,该编译器是针对该主机的交叉编译器。 这样,由于该软件是使用该主机的目标交叉编译器编译的,因此该软件将能够在该主机上运行,即使它构建在(可能)不同的构建机器上。
具体事物的例子
您的发行版配备了一个编译器,该编译器以您的MachineSearch distributionNativeExecFormat发行版为目标,并在同一台您的MachineSearch distributionNativeExecFormat发行版计算机上运行。 它是在某些机器上构建的(可能是在m68k macho osx或其他类似的彩色机器上构建的),带有一个针对您的MachineSearch distributionNativeExecFormat发行版的交叉编译器,在构建过程中,它自己的“目标”设置为您的MachineSearch distributionNativeExecFormat发行版,因此,它现在既可以运行,也可以为您的MachineSearch distributionNativeExecFormat分发吐出可执行文件。
交叉编译器对于完全明确地针对特定机器非常有用。 当您开始为独立于本机发行版的股票编译器的机器开发时,您将需要一个交叉编译器。 实际上,从您第一次开始尝试生成完全独立的可执行文件(即,当您在进行OSDev时)开始,您就已经*确实*针对一台与本机不同的机器。
本机编译器以MachineSearch分发版NativeExecFormat分发版为目标。 但是你想以你的目标为目标搜索你选择的格式无;其中,“none”作为操作系统名称表示没有系统库的机器。 编译内核时,您希望使用不知道目标机器的任何系统库的编译器进行编译。 内核必须完全独立。 内核所需的一切都必须在其源代码树中,这样就不需要隐式系统库(或者在您不知情的情况下链接)。
稍后,当您有一个用户空间和一组系统库供您的程序链接时,您将构建一个针对以下对象的编译器:yourOsTargetArch yourChosenFormat yourOs,同时保留针对yourOsTargetArch yourChosenFormat none的编译器。 两者之间的区别在于,“无”操作系统目标编译器在其/lib目录中没有任何库,也没有可链接的系统库。 因此,使用这种“裸机”目标编译器编译的任何软件都不可能具有未知的依赖项。 以交叉编译器为目标的“yourOS”是您放置自己的系统库(在其/lib目录中)的地方,这样,当它构建软件时,它会将它们与这些系统库链接,从而将它们与内核API链接。 您将继续使用“基本”目标编译器来构建内核,并且每当您在开发机器上构建预期在内核目标机器上运行的程序时,您都将使用yourOsTargetArch yourChosenFormat yourOs编译器,它有一个/include和/lib目录,其中充满了您自己操作系统的本机系统包含和库。
稍后,当您确信可以从内核内部开发和使用内核时,您将希望停止在单独的“开发”机器上构建所有程序。 更具体地说,在这一点上,您希望您的“构建”机器与您的测试机器相同,这样您就可以从操作系统内部构建在操作系统上运行的程序。 此时,是采取最后一步的时候了,从您单独的“开发”机器上,构建一个将在您的操作系统上运行的编译器(host=yourOsTargetArch yourChosenFormat yourOs),并以您的操作系统为目标(target=yourOsTargetArch yourChosenFormat yourOs)。这个交叉编译器,即加拿大交叉编译器,允许您在自己的操作系统用户空间上运行时以本机方式编译其他程序。 它本质上是一个“发行版原生编译器”,就像发行版附带的编译器一样。 从那时起,如果有一个编译器运行在你的操作系统上并以你的操作系统为目标,你就可以更自由地移植程序,并允许人们在运行你的操作系统时做同样的事情,假设你用你的操作系统打包了这个本机编译器。
我什么时候不需要交叉编译器?
如果您创建了一个真正的操作系统,并设法将gcc移植到它,那么gcc将生成与i686 myos gcc完全相同的代码。 这意味着您不需要在自己的操作系统上使用交叉编译器,因为那里的gcc已经做了正确的事情。 This is why the Linux kernel is built with the Linux gcc, instead of a Linux cross-compiler.
一个具体的例子
- gcc -v* from my Ubuntu machine gives:
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)
The person who built the compiler that runs on my machine built it on a machine just like mine, an 'i486-linux-gnu' (build), and intended for it to run on a machine like his/mine: the same i486-linux-gnu (host), and he meant for this compiler he was building for me, to emit executables that targeted my very machine, so that when I compile programs, they will be able to run on a host that is i486-linux-gnu. Therefore he made the compiler target i486-linux-gnu.