- 1 MIPS架构移植软件时常见的问题
- 2 什么是字节序:WORD、BYTE和BIT
- 2.1 位、字节、字和整形
- 3 软件和字节序
- 3.1 可移植性和字节序
- 4 硬件和字节序
- 4.1 建立连接字节序不一致的总线
- 4.2 建立字节序可配置的连接
- 4.3 对字节序问题的一些错误认知
- 5 在MIPS架构上编写支持任意字节序的软件
- 6 可移植性和大小端无关代码
站在巨人的肩膀上,才能看得更远。 If I have seen further, it is by standing on the shoulders of giants. 牛顿
科学巨匠尚且如此,何况芸芸众生呢。我们不可能每个软件都从头开始搞起。大部分时候,我们都是利用已有的软件,不管是应用软件,还是操作系统。所以,对于MIPS架构来说,完全可以把在其它架构上运行的软件拿来为其所用。
但是,这是一个说简单也简单,说复杂也复杂的工作。为什么这么说呢?如果你要采用的软件,其可移植性比较好的话,可能只需要使用支持MIPS架构的编译器重新编译一遍就可以了;如果程序只是为特定的硬件平台编写的话(大部分嵌入式软件都是如此),可能处处是坑。而像Linux
系统,在编写应用或者系统软件的时候,一般都会考虑可移植性。所以说,基于Linux的软件一般都可以直接编译使用。但是,像现在流行的一些实时操作系统,比如、μC/OS
、Free-RTOS
、RT-Thread
或其它一些基于微内核的系统,它们的程序一般不通用,需要修改才能在其它平台上运行。
而且,越往底层越难移植,几乎所有嵌入式系统上的驱动程序都不能直接使用。而且,嵌入式系统软件通常好几年才会发生一次重大设计更新,所以,如果坚持考虑软硬件上的接口兼容并不合理,尤其是考虑到成本效益的时候。
本文就是总结一些在移植代码或者编写代码时,应该需要特别关注的一些点。
虽说本文主要以MIPS架构为主线进行讲解,但是其中的一些思想和方法,对其它架构同样适用。我们应该学会举一反三,灵活运用。
1 MIPS架构移植软件时常见的问题
以下是一些比较常见的问题:
- 大小端
计算机的世界分为大端(
big-endian
)和小端(little-endian
)两个阵营。为了二者兼容,MIPS架构一般都可以配置到底使用大端还是小端模式。所以,我们应该彻底理解这个问题,不要在这个问题上栽跟头。 - 内存布局和对齐 大部分时候,我们可以假定C声明的数据结构在内存中的布局是不可移植的。比如,使用C的结构体表示从输入文件或者网络上接收的数据的时候。还有,对于指针或者union型数据,通过不同方法引用的时候,也会存在风险。但是,内存布局还与一些其它的约定有关(比如寄存器的使用,参数传递和堆栈等)。
- 管理Cache 对于嵌入式系统来说,大部分时候采用的都是微处理器,可能并没有实现硬件Cache。但是,随着半导体技术的发展,现在的高端工业处理器一般都带有Cache,只是对于系统软件来说是不可见而已(比如,大部分处理器把Cahce可能带有的副作用都由硬件进行处理,软件不需要管理)。但是,大部分MIPS架构的CPU为了保持硬件的简单,而将一些Cache的副作用暴漏给软件,需要软件进行处理。关于这部分内容,我们后面会进行阐述。
- 内存访问序 在大部分的嵌入式或者消费电子产品中,一般都挂载了许多子系统,这些子系统一般通过一条总线,比如PCIe总线、AHB总线、APB总线等进行通信。虽然方便了我们对系统进行扩展,但是也带来了不可预知的问题。比如,CPU和I/O设备之间的信息需要缓存处理,招致不可见的延时;或者它们被拆分成几个数据流,扔到总线上,但是对于到达目的地的顺序却没有保障。关于这部分内容,我们后面会进行阐述。
- 编程语言 对于语言,当然大部分时候使用C语言了。但是,对于MIPS架构来说,有些事情可能使用汇编语言编写更好。讲解这部分内容的时候,主要涉及inline汇编、内存映射I/O寄存器和MIPS架构可能出现的各种缺陷。
2 什么是字节序:WORD、BYTE和BIT
WORD
最早是由Danny Cohen
在1980年引入计算机科学的。在他的文章中,以其独有的幽默和智慧指出,通信系统分为两大阵营,分别是字节寻址访问和整数寻址访问。
在乔纳森·斯威夫特(Jonathan Swift)的《格列佛游记》(Gulliver’s Travels)中,little-endians
派和big-endians
派就如何吃一个煮熟的鸡蛋展开了一场战争。斯威夫特讽刺的是18世纪的宗教争端问题,双方都不知道他们的分歧是完全武断的。科恩的笑话很受欢迎,这个词也就流传了下来。这个问题不仅仅体现在通信上,对于代码的可移植性也有影响。
计算机程序总是在处理不同类型的数据序列:迭代字符串中的字符,数组中的WORD类型元素,以及二进制表示的BIT位。C程序员普遍认为,所有这些变量以字节为单位在内存中顺序排列的-比如,memcpy()函数能够复制任何数据,不论什么数据类型。而且,使用C语言编写的I/O系统也将I/O操作以字节进行建模,你才能够使用read()和write()之类的函数读写包含任何数据类型的内存块。
这样,一个计算机写数据,另一个计算机读数据。那么,我们不禁想,第二台计算机是如何理解第一台计算所写的数据的呢?
另外,我们不止一次地被提醒,要小心数据填充和对齐。因为这对于数据搬运会产生很大的影响。比如说,因为填充的原因,想要完整准确地传递float型数据就变得很难,所以,浮点数据存在精度问题。但是,我们期望至少能够正确表述整形数据,而”字节序”就是个拦路虎。比如说,一个32位整型数,用16进制进行表示为0x12345678
,而读进来却为0x78563412
,发生了字节交换。想要理解为什么,我们需要追溯一下字节序的发展历史。
2.1 位、字节、字和整形
我们知道一个32位的int型数据,是由32个比特位组成的,它们每一位都有自己的意义,就像我们熟悉的10进制那样,每一位分别表示个
、十
、百
、千
、…以此类推,对于二进制,bit0代表1,bit1代表2,bit3代表4,bit4代表8,…。对于一个可以按字节访问的内存来说,32位整数占据4个字节。如何从比特位的视角表述整形数,有两种选择:一派,将低有效位(LS)放在前,也就是存储在内存的低地址里;而另一派,将高有效位(MS
)放在前。科恩将其分别称为小端和大端。最早的时候,DEC的微型计算机是小端,而IBM的大型机是大端。彼时,两个阵营互不妥协。
有一个细节需要特殊提一下,大小端字节序的问题只有能够按字节访问的时候才会有。1960年代之前的电脑都是按照WORD大小进行组织:包括指令,整型数和内存宽度都是WORD大小。所以,不存在字节序的大小端问题。
我们在读写10进制数据的时候,习惯于从左到右,高有效位在左,低有效位在右。BYTE最早引入计算机,是为了方便将CHAR型字符打包成WORD,然后进行数据的交互。1970年代,一位IBM的老工程师花费了大量的时间,研究大量的内存dump列表,每个WORD大小的数据代表一组字符。这样看起来,使用小端字节序没有必要。大端字节序更有利于使用和阅读。但是,将数字的高有效位写在左端,字节顺序也是自左向右增加,这样和从右到左对bit位进行编号的行为不一致。于是,IBM将一个高有效位标记为bit0。看起来如下图所示:
但是,根据数据的算术意义对bit位进行编号更自然,也就是说,标记为N
的bit位,其算术意义就是2^N
。这样,就可以把bit0-7存储在字节0
中。显然,这种方式就变成了小端模式。显然,这种方式不利于阅读,但是对于习惯于将内存看成是一个字节型的大数组的人来说,就会非常有意义。
通过上面的讨论,可以看出,两幅图中,内容都是相同的,只是最高有效位(MS)和最低有效位(LS)进行了互换,当然,bit位的顺序也发生了互换。IBM主导的大端模式,看到的是被分割成字节的WORD;而Intel主导的小端模式看到的是构建WORD的字节序列。毋庸置疑的是,对于不同的人群,它们都非常有用。它们都有自己的优点,就看你怎么选择了。
让我们回到上面的问题。假设一个16进制类型的数据0x12345678
,二进制形式为00010010 00110100 01010110 01111000
。如果传送给一个具有相反字节序的系统,你肯定期望看到所有的位是相反的:00011110 01101010 00101100 01001000
,16进制为0x1E6A2C48
。但是,为什么我们上边却说是0x78563412
。
的确,在某些情况下完全可以实现上面的位反转:有些通讯链路先发送最高有效位,另一些则先发送最低有效位。但是,在上世纪70年代,更多地使用8位的字节作为计算机内部和计算机通信系统的基本单元。通常,通信系统使用字节构建消息流,由硬件决定哪一位首先被发送出去。
与此同时,每个微控制器系统都使用8位宽的外设控制器(更宽的控制器是为高端设备预留的),这些外设一般都使用8位端口,bit0-bit7,最高有效位是bit7。对此,没有任何争议,每个字节都采用小端字节序。从那以后,一直保持到现在。
而早期的微处理器系统,都是8位CPU,使用8位总线和一个8位的内存进行通信,所以,根本不存在字节序问题。Intel的8086是一个16位的小端系统。当摩托罗拉在1978年左右推出68000微处理器时,他们推崇IBM的大型机架构。不管是处于对IBM的敬仰,还是为了区别于Intel,他们选择了大端模式。但是,它们无法违反8位外设控制器的习惯,于是,每一个8位的摩托罗拉的外设通过交错的数据总线与68000进行连接。这就是,我们为什么说收到0x78563412
数据的原因。于是,68000家族系列使用如下图所示的字节序:
68000及其后继产品被大多数成功的UNIX服务器和工作站所使用(尤其是SUN公司)。所以,当MIPS架构和其它RISC指令集架构的CPU在1980年代出现时,他们的设计者为了兼容大小端字节序,都设置了配置选项,可以自由选择使用大小端模式。但是,从68000开始,大端模式就指68000风格的大端字节序,其bit位和字节序相反。当你配置MIPS架构CPU为大端模式时,就如上图所示。
选择不同的大小端模式,可能会影响你阅读CPU和寄存器手册。尤其是对于位操作指令,向左移动和向右移动的区别,位操作指令的参数位置等。
通过上面的讨论,我们知道,大小端字节序对于软硬件的影响分为2类:软件的话,比如移植软件和数据通信;硬件的话,如不兼容子系统和总线之间的连接问题。对此,我们分别进行阐述。
3 软件和字节序
对于软件来说,字节序的定义如下:如果CPU或编译器中,一个整型数的最低寻址字节存储的是最低8位,那么就是小端模式;如果最低寻址字节存储的是最高8位,那么就是大端模式。可以通过下面的代码,验证你的CPU是大端还是小端模式。
代码语言:javascript复制#include <stdio.h>
main ()
{
union {
int as_int;
short as_short[2];
char as_char[4];
} either;
either.as_int = 0x12345678;
if (sizeof(int) == 4 && either.as_char[0] == 0x78) {
printf ("Little endiann");
}
else if (sizeof(int) == 4 && either.as_char[0] == 0x12) {
printf ("Big endiann");
}
else {
printf ("Confusedn");
}
}
严格说来,软件字节序是编译器工具链的一个属性。只要你愿意,可以产生任何字节序的代码。但是对于像MIPS架构这样的可字节寻址的CPU,内部使用32位算术运算,这会导致硬件效率降低;因此,我们接下来,主要谈论的是CPU的字节序。
当然了,内存地址空间中字节布局的问题也同样适用于其它数据类型。比如浮点数据类型,文本字符串,甚至是机器指令的32位操作码。对于这些非整型数据类型来说,算术意义根本没有存在的价值。
当软件要处理的数据类型大于硬件能够管理的数据类型时,字节序问题完全就成为软件的一种约定了,可以是任何字节序。当然了,最好还是与硬件本身的约定保持一致。
3.1 可移植性和字节序
只要应用程序不从外界获取数据,或避免使用不同的整型数据类型访问同一个数据块(如上面我们故意那样做的那样),CPU的字节序对你的应用程序就是不可见的。也就是说,你的代码是可移植的。
但是,应用程序不可能接受这些限制。你可能必须处理外部发送过来的数据,或者需要把硬件寄存器映射到内存上,便于访问。不管哪种应用,你都需要准确知道编译器如何访问内存。
这好像没有什么,但是经验告诉我们,字节序是最容易混淆的,因为很难描述这个问题。大小端两种方案起源于勾画和描述数据的不同方式,它们在各自的视角都没有什么问题。
如上所述,大端模式通常围绕WORD来组织其数据结构。如下图1所示。虽然按照IBM约定,将最高有效位(MS)标记为位0更为美观,但是,现在已经不在那样做了。
而小端模式更主要从软件方面抽象数据结构,将计算机的内存视为一个字节类型的数组。如上图2所示。小端模式没有将数据看作是数值型的,所以倾向于把低有效位存放在左边。
所以,软件大小端的字节序问题,归根结底就是一个习惯的问题:究竟习惯于从左到右,还是从右到左对bit位进行编号。每个人的习惯不同,这也是字节序问题容易混淆的根源。
4 硬件和字节序
前面我们已经看见,CPU内部的字节序问题,只有在能够同时提供WORD字长的数据和按字节访问的内存系统中才会出现。同样,当系统与具有多字节宽度的总线进行连接时,也会存在字节序问题。
当通过总线传输多个字节数据时,数据中的每个字节都有自己的存储地址。如果总线上传输数据的低地址字节,被编为低编号,那么这条总线就是小端模式;反之,如果使用高编号对数据的低地址字节进行编号,那么就是大端模式总线。
可字节寻址的CPU,在它们传送数据的时候会声明是大端还是小端字节序。英特尔和DEC的CPU是小端模式;摩托罗拉680x0和IBM的CPU是大端模式。MIPS架构CPU可以支持大小端两种模式,需要上电时进行配置。许多其它RISC指令集架构的CPU也都遵循MIPS架构的思路,选择大小端可配置的方式:这在使用一个新的CPU替换已经存在的系统时是个优点,如果旧系统遵循小端模式,新的CPU也配置为小端模式;反之亦然。
假设硬件工程师按照比特位的顺序把系统串联在一起,这本身没有什么错。但是,如果你的系统包含总线、CPU和外设,而它们的字节序不匹配时,会很麻烦。只能哪种方式更简单一些,使用哪一种。
- 位顺序一致/字节序被打乱 很显然,设计者可以按照位顺序的方式,把两条总线接到一起。这样,每个WORD的位顺序没有变化,但是位编号和字节是不同的,那么,两边内存中的字节序列也是不同的。 任何小于总线宽度或没有按照总线宽度进行排列的数据,在总线上传输时,都会被破坏顺序,并按照总线宽度发生字节交换。这看上去要比软件问题严重。软件产生错误字节序的数据,根据数据类型仍能找到,因为没有破坏数据类型的边界;这是这个数据已经没有意义。但是,硬件却会打乱数据类型的边界(除非,数据恰好以总线宽度对其)。 这儿有一个问题。如果通过总线接口进行传输的数据,总是按照WORD大小对齐,然后按照比特位的编号进行接线,那么就会隐藏大小端字节序的问题。也就避免了软件再对其进行转换。但是,硬件工程师很难知道,设计的系统上的接口以后会传输什么数据。所以,应该小心应对这个问题。
- 字节地址一致/整数被打乱 设计者可以按字节地址进行连线,也就是保证两端的相同字节存储在相同的地址。这样,字节通道内的比特位的顺序必然不一致。至少,整个系统可以把数据看作字节数组,只是数组元素的比特位是相反的。
对于大多数情况下,字节地址乱序副作用更明显。所以,我们推荐使用“字节地址一致”方法进行连线。因为在处理、传输数据时,程序员更希望将内存看作为字节数组。其它数据类型一般也是据此构建的。
不幸的是,有时候使用位编号一致,好像在原理图上更为自然。想要说服硬件工程师修改他们的原理图是一件很难的事情哦。这个大家都懂的