做嵌入式开发过程中常常提到交叉编译,这到底是怎么回事?本文从先介绍一些作者自己对计算的一些思考。

1. 从计算说起

多读几下计算机这个名字,我们大概会去想这应该是一个机器、用来做计算的,它应该和我们理解的加减乘除四则运算有关系。应该是一个机器可以帮我们做一些运算。运算是啥意思呢?就是从一些信息的到另外一些信息的过程。

  1. 把遇到的问题抽象出一个模型。
  2. 把这个抽象的模型映射到另一个实际的事物上。
  3. 用这个实际事物一些特性“自然地”的到一些东西。
  4. 把的到的东西转换为遇到的问题的结果。

举个例子:

现在要请人吃饭,需要准备筷子,已经知道一人需要一双筷子(就是两支),现在来了A个人,一共需要多少支筷子。(这是具体问题)

当然,聪明如你,肯定知道需要2A支筷子。为了增加一下难度,现在来了33223493个人,你能一下算出结果吗?大概不太能很快的到结果。此时需要借助一些工具,如草稿纸,如计算器。在草稿纸或者计算器上进行一个运算。(这里就把要准备多少支筷子这个事情转换为草稿纸计算或者计算器计算这个过程了,实现了转换到另一个实际事物上了。)

你学过四则运算,你肯定会用竖式计算,得到一个结果。如果你决定充分利用现代科技的技术,你会自然在计算器上按下对应的按键,的到结果。 总之,这两种方式都让你自然地的到了一个结果。

最后你根据草稿纸或者计算器上显示的数字,得到了要准备的筷子数目。

这里我们从总人数,得到了要准备的筷子数目:这就是计算过程。

当然,上面是一个简单的例子,简单到让我们会以为我们的现代科技也不过如此。但是实际上计算机面对的问题可比这个复杂。

  1. 我们稍微增加一下问题,就是说一支筷子0.5元,一共要多少钱?
  2. 考虑到薄利多销,如果数目超过1000支不超过2000支的打9折,2000~3000打8折,请问一共要多少钱?
  3. 出现了一个竞争卖家,优惠规则是一律88折,请问应该选哪一家?
  4. ......

诸如此类问题可以一直问下去。相信你已经通过这些问题,学会了计算这个过程了,请帮我计算一下明天是否会下雨这个问题。

毕竟小学二年级就已经开始算术入门了。

这里说到几个关键的步骤,建立模型,这就是一堆算法研究员干的工作。 自然得到另外一个结果,这就是计算机和电子工程在研究的事情。

这两个过程其实是强耦合的。

这里再来解释一下Hello World的事情,它是大多人接触编程的初体验:

print("Hello World")

按下回车,计算机就乖乖在屏幕上输出 Hello World。其实我们有如下看似非常自然的共识: 1. 在屏幕上显示 Hello World这样的图案,我们就认为运行成功了。

但是实际上,我们并没有说到底是白底黑字还是黑底白字。

但计算机怎么就读懂了我们的指令?

计算机并不能看懂我们写的abcd,它只能看懂0101序列。0101序列

0101序列和abcd的区别是啥,为啥能看懂数字而不能看懂字母?

本质上来说,计算机也看不懂0101序列,它只能理解电子元器件的物理状态。0101序列是人们对电子元器件物理状态的抽象。

电子计算机由电子元器件实现,最核心部件是晶体管。晶体管作为一种电子器件,最重要的参数是电压电流。电子元器件通电,即可在电子元器件不同的地方测量电压和电流,这是电子元器件最基本的特性。而这些电压电流就可以用来对世界进行抽象。

C 语言的教材告诉我们,我们写的代码会被编译为计算机能够识别的二进制语言,然后才能在计算机上运行。也许你听说过,chrome 从源代码编译,需要很长时间。那么编译是什么呢?

计算机代码编译,把人类语言翻译为计算机能看懂的语言

mosfet

mosfet,在不同端口施加电压,可以在另外端口测得电压,另外端口的电压和在当前端口施加电压大小有关系 制造 cpu 时,会用一种非常重要的器件 mosfet。其特点是:

  • 在一个端口施加电压(记为输入电压),另外端口也可以测量到电压(记为输出电压)
  • 输入电压变化,则输出电压会(自动)变化

电压是连续变化的物理量,在计算机内部处理并不方便。此外精确的电压也不好控制。虽然到控制电压到某个精准值很困难,但是让电压低于一个值(不管低多少,只要低就行)或者高于一个值(不管高多少,只要高就行),这个在物理上实现起来就比较容易。比如大学考试,不挂科(不小于 60)可比恰好及格容易。

通常把电压高于一个值的状态叫做高电平,低于一个值的状态做低电平。其中高电平和低电平的状态是可以改变的。其中高电平和低电平之间是泾渭分明的,高电平的最小值一定要比低电平的最高值要大。充分考虑元器件的电气特性,并且精心设计,这一切都是可以实现的。

对于这些元器件来说,它们在高低电平状态之间的跳转,是物理规律。只要它输入电平在某个状态,就一定让输出状态是一个确定的状态,用电子元器件的物理行为来模拟计算过程。这就是计算机计算的本质。

所谓0101序列,描述的就是计算机中复杂的元器件的电平高低状态。01和高低状态之间没有绝对关系,可以用1表示高电平0表示低电平,也可以1表示低电平0表示高电平。不过一旦确定,就要按照确定的规则来编写程序,否则就会出错。

不同的 cpu 架构就存在这诸如高低电平表示方式不一样的问题。当然,高低电平只是一种差异,还有比如指令集不一样的差异等。这都导致,不同 cpu,在输入同样电平序列时候,输出可能不一样。最后展示在输出平台,如屏幕上,得到的结果不一样。

其实在计算机内部都是一系列状态的集合,不管是源代码还是 01 序列,计算机其实是分不清楚的。我们只是进行了抽象,就解决了这些问题。为了实现人类可以看懂,序列的排布可能并不适合 cpu 运算出结构。所以需要编译,把这些命令转换为适合计算机运算时候的电平状态,这就是编译干的事情。

当然,这个转换过程十分复杂,上面只是简单地介绍。这里想说明的问题是,人眼看起来相同的源代码,在不同平台上,实际上是不同的。翻译为最终机器可以运行的电平状态,也是不同的。这也是不同平台可执行文件不一定能够通用的原因。

举一个不是那么恰当的例子,如果你肚子饿了,你想吃东西,你在中国就说:我饿了,我要吃东;但如果你到了美国,你可能要讲 I am hungry, and I want some food. 如果在中国讲美国话,或者在美国讲中国话,大概率会进行得不那么顺利,甚至要一直挨饿。应为人家听不懂你在说啥呀。

1.2 不同架构,可执行文件不一样

架构的意思就是 cpu 的结构和规则,比如说 cpu 的指令是是 32 位还是 64 位,cpu 指令集是 cisc 还是 risc。如果要简单类比,就是有中国打工人,美国打工人。计算机有不同的架构(确切说是 CPU):

  • x86,这是大多数 pc 的平台
  • arm 平台,多用在嵌入式设备,以及大部分智能手机
  • IBM 的 powerPC 架构
  • mips,国产龙芯用的这个
  • RISCV 最近非常火

不同架构之间的可执行文件通常是不通用的,最常见的是 windows 上的应用不能直接拿到 linux 上来用,ios 上的应用不能拿到安卓上的来用。

不同架构有性能功耗的差异,针对不同应用场景。总之,就出现了有不同架构这么一个事实。一句话总结,计算机架构不同是有历史原因的。

交叉编译,重点就在这个交叉上。就是在一种规则下,通晓了另一种规则。就是要理解,不同计算机的架构是不一样的。

2. 编译也是个体力活,交叉编译解决大问题

不同 cpu 架构不一样,编译出来的可执行文件不通用不仅不奇怪,而且理所当然。不同平台有不同优势。有的架构计算能力强,可以用来编译;有的架构功耗低,用来运行简单程序,毕竟可以让你多用一会儿。

当然我们又想马儿跑,又想马儿不吃草。其实也是可以的,就是有点麻烦,这就是交叉编译,在一个平台编译可以在另外一个平台运行的可执行文件。

比如说可以在 x86 平台上编译能够在 arm 平台上运行的可执行文件。本质上,就是在 x86 平台上,按照 arm 平台运行的规则翻译了可执行文件。这时候编译出的结果,反而不能在 x86 平台上来跑。

举个例子,交叉编译就是一个美国人到中国来用中文考试,考试结果其它美国人看不懂,他自己虽然能看懂但也没有什么用,但是中国人可以看懂的。不过他考试的方式是先用英文作答,然后用翻译软件翻译为中文。

3. x86 编译 arm 编译器介绍

这部分来自这里,但似乎链接挂了,这里进行一下摘抄吧。

交叉编译工具链的命名规则为:arch [-vendor] [-os] [-(gnu)eabi]

  • arch - 体系架构,如 ARM,MIPS
  • verdor - 工具链提供商
  • os - 目标操作系统
  • eabi - 嵌入式应用二进制接口

根据对操作系统的支持与否,ARM GCC 可分为支持和不支持操作系统,如

  • arm-none-eabi:这个是没有操作系统的,自然不可能支持那些跟操作系统关系密切的函数,比如 fork(2),它使用的是 newlib 这个专用于嵌入式系统的 C 库。
  • arm-none-linux-eabi:用于 Linux 的,使用 Glibc

3.1 交叉编译例子

3.1.1 arm-none-eabi-gcc

(ARM architecture,no vendor,not target an operating system,complies with the ARM EABI)用于编译 ARM 架构的裸机系统(包括 ARM Linux 的 boot、kernel,不适用编译 Linux 应用 Application),一般适合 ARM7、Cortex-M 和 Cortex-R 内核的芯片使用,所以不支持那些跟操作系统关系密切的函数,比如 fork(2),他使用的是 newlib 这个专用于嵌入式系统的 C 库。

3.1.2 arm-none-linux-gnueabi-gcc

(ARM architecture, no vendor, creates binaries that run on the Linux operating system, and uses the GNU EABI)

主要用于基于 ARM 架构的 Linux 系统,可用于编译 ARM 架构的 u-boot、Linux 内核、linux 应用等。 arm-none-linux-gnueabi 基于 GCC,使用 Glibc 库,经过 Codesourcery 公司优化过推出的编译器。 arm-none-linux-gnueabi-xxx 交叉编译工具的浮点运算非常优秀。一般 ARM9、ARM11、Cortex-A 内核,带有 Linux 操作系统的会用到。

3.1.3 arm-eabi-gcc

Android ARM 编译器。

3.1.4 armcc

ARM 公司推出的编译工具,功能和 arm-none-eabi 类似,可以编译裸机程序(u-boot、kernel),但是不能编译 Linux 应用程序。armcc 一般和 ARM 开发工具一起,Keil MDK、ADS、RVDS 和 DS-5 中的编译器都是 armcc,所以 armcc 编译器都是收费的(爱国版除外,呵呵~~)。

3.1.5 arm-none-uclinuxeabi-gcc 和 arm-none-symbianelf-gcc

arm-none-uclinuxeabi 用于 uCLinux,使用 Glibc。 arm-none-symbianelf 用于 symbian,没用过,不知道 C 库是什么 。

3.2 Codesourcery 产品

Codesourcery 推出的产品叫 Sourcery G++ Lite Edition,其中基于 command-line 的编译器是免费的,在官网上可以下载,而其中包含的 IDE 和 debug 工具是收费的,当然也有 30 天试用版本的。

目前 CodeSourcery 已经由明导国际(Mentor Graphics)收购,所以原本的网站风格已经全部变为 Mentor 样式,但是 Sourcery G++ Lite Edition 同样可以注册后免费下载。

Codesourcery 一直是在做 ARM 目标 GCC 的开发和优化,它的 ARM GCC 在目前在市场上非常优秀,很多 patch 可能还没被 gcc 接受,所以还是应该直接用它的(而且他提供 Windows 下 mingw 交叉编译的和 Linux 下的二进制版本,比较方便;如果不是很有时间和兴趣,不建议下载 src 源码包自己编译,很麻烦,Codesourcery 给的 shell 脚本很多时候根本没办法直接用,得自行提取关键的部分手工执行,又费精力又费时间,如果想知道细节,其实不用自己编译一遍,看看他是用什么步骤构建的即可,如果你对交叉编译器感兴趣的话。

3.3 ABI 和 EABI

ABI:二进制应用程序接口(Application Binary Interface (ABI) for the ARM Architecture)。在计算机中,应用二进制接口描述了应用程序(或者其他类型)和操作系统之间或其他应用程序的低级接口。

EABI:嵌入式 ABI。嵌入式应用二进制接口指定了文件格式、数据类型、寄存器使用、堆积组织优化和在一个嵌入式软件中的参数的标准约定。开发者使用自己的汇编语言也可以使用 EABI 作为与兼容的编译器生成的汇编语言的接口。

两者主要区别是,ABI 是计算机上的,EABI 是嵌入式平台上(如 ARM,MIPS 等)。

3.2.1 arm-linux-gnueabi-gcc 和 arm-linux-gnueabihf-gcc:

两个交叉编译器分别适用于 armel 和 armhf 两个不同的架构,armel 和 armhf 这两种架构在对待浮点运算采取了不同的策略(有 fpu 的 arm 才能支持这两种浮点运算策略)。

其实这两个交叉编译器只不过是 gcc 的选项 -mfloat-abi 的默认值不同。gcc 的选项 -mfloat-abi 有三种值 soft、softfp、hard(其中后两者都要求 arm 里有 fpu 浮点运算单元,soft 与后两者是兼容的,但 softfp 和 hard 两种模式互不兼容):

  • soft: 不用 fpu 进行浮点计算,即使有 fpu 浮点运算单元也不用,而是使用软件模式。
  • softfp: armel 架构(对应的编译器为 arm-linux-gnueabi-gcc )采用的默认值,用 fpu 计算,但是传参数用普通寄存器传,这样中断的时候,只需要保存普通寄存器,中断负荷小,但是参数需要转换成浮点的再计算。
  • hard: armhf 架构(对应的编译器 arm-linux-gnueabihf-gcc )采用的默认值,用 fpu 计算,传参数也用 fpu 中的浮点寄存器传,省去了转换,性能最好,但是中断负荷高。

总结

本文从概念上简单说明了计算机编译个过程,详细解释了交叉编译要做的事情,最后给出了交叉编译说明的相关链接。讲述过程可能并不严谨,主要目的是说明清楚这些问题。