移植性问题包含的内容很广泛,本节要介绍的是代码在不同体系结构下的移植问题。Linux内核是高度可移植的,若希望在不同平台下开发的应用程序或设备驱动程序也能很好地兼容,这就要求用户在开发的过程中要充分考虑与移植相关的内容。本文将讨论数据类型、数据对齐,以及与字节顺序相关的移植性问题。


1 字长和数据类型

不同的体系结构具有不同的字长,表1列出了一些常见体系结构的字长。

表1 表示例



体 系 结 构



字 长



alpha



64



ia64



64



mips64



64



powerpc64



64



sparc64



64



x86_64



64



um



32/64



s390



32/64



arm



32




体 系 结 构



字 长



h8300



32



i386



32



m32r



32



m68r



32



mips



32



powerpc



32



v850



32



因此在不同体系结构下C语言数据类型的大小不是相同的,用户必须安排特定大小的数据项,才能更好地进行移植。

在编写程序代码之前,有必要了解各种常用数据类型的长度,比如说char、short、int和long的长度。

● char类型的长度被定义为8字节。

● short类型的长度被定义为至少2字节。因此在有些计算机上,对于某些编译器,short类型的长度可能就是4个字节,甚至更长。

● int类型是一个整数的“自然”大小,其长度至少为2字节,并且至少要和short类型一样长。在16位计算机上,int类型的长度可能为2字节;在32位计算机上,可能为4字节;当64位计算机流行起来后,int类型的长度可能会达到8字节。例如,早期的Motorala 68000是一种16/32位的混合型计算机,依赖于不同的命令行选项,一个68000编译程序能产生两个字节长或4字节长的int类型。

● long类型至少和int类型一样长(因此,它也至少和short类型一样长)。long类型的长度至少为4字节。32位计算机上的编译程序可能会使short、int和long类型的长度都为4字节。

如果需要一个4字节长的整型变量,不能想当然地以为int或long类型能满足要求,而要用sizeof()来检测int和 long的长度;再根据检测的结果,使用typedef把一种固有的类型定义为用户所需要长度的类型,并在其前后加上相应的#ifdef指令。

#ifdef four_Byte_long

typedef long int4;

#endif

Linux内核,在/usr/src/linux/include/asm/types.h文件中也定义了一些长度确定的数据类型。

typedef unsigned short umode_t;

typedef __signed__ char __s8; //带符号字节

typedef unsigned char __u8; //无符号字节

typedef __signed__ short __s16; //带符号16位整型

typedef unsigned short __u16; //无符号16位整型

typedef __signed__ int __s32; //带符号32位整型

typedef unsigned int __u32; //无符号32位整型

#if defined(__GNUC__) && !defined(__STRICT_ANSI__)

typedef __signed__ long long __s64; //带符号64位整型

typedef unsigned long long __u64; //无符号64位整型

#endif

2 数据对齐

编写可移植代码而值得考虑的一个问题是如何存取不对齐的数据。例如,如何读取存储在一个不是 4 字节倍数的地址的4字节值。i386用户常常存取不对齐数据项,必须注意,并不是所有的体系结构都允许。很多现代的体系会在发生上述事件时产生一个异常,每次程序试图进行不对齐数据传送时,数据传输由异常处理来处理,带来很大的性能牺牲。如果用户需要存取不对齐的数据,应当使用下列宏:

#include

get_unaligned(ptr);

put_unaligned(val, ptr);

这些宏是无类型的,并且用在每个数据项,不管它是1字节、2字节、4字节或者8字节。

进行不对齐的数据操作严重影响系统的性能,虽然在现代的系统里采用异常处理机制,但不是通用的方法,比如在sparc或者MIPS上发生不对齐数据操作时就发生总线错误。

不对齐虽然能节省内存空间,但是不适合移植性编程,为了编写的程序可以跨平台移植,必须使用数据项对齐。

在数据对齐的处理上,编译器的作用也需要注意,同样的数据结构可能在不同的平台上进行不同的编译。编译器可能根据各平台不同的规则来安排结构的成员对齐。因此数据对齐不仅依赖处理器架构,也依赖于编译器的具体操作。

下面来分析一个数据对齐的例子。

结构体定义如下:

struct A

{

int x;

char y;

short z;

};

struct B

{

char y;

int x;

short z;

};

硬件平台:32bit,x86,GCC编译器。

先熟悉硬件平台上各数据类型的数据长度。

char:1B

short:2B

int:4B

long:4B

float:4B

double:8B

对结构体A、B分别使用sizeof()函数求长度,结果是:

● sizeof( struct A)得到的值是8;

● sizeof( struct A)得到的值是12。

从表面上看,结构体A和结构体B应该具有相同的长度。之所以发生上面的变化是因为编译器的作用,编译器默认对数据成员在空间上进行了对齐。用户也可以更改编译器的默认设置。

使用指定对齐值来修改#pragma pack (value)的指定对齐值value。

例如:

#pragma pack( 2 )

//指定下列数据按照两个字节对齐

struct C

{

char y;

int x;

short z;

};

//取消指定对齐,恢复默认方式对齐

#pragma pack()

此时sizeof( struct C )的值为8。

选择#pragma pack (value)中的value值为1。

#pragma pack( 1 )

//指定下列数据按照两个字节对齐

struct E

{

char y;

int x;

short z;

};

//取消指定对齐,恢复默认方式对齐

#pragma pack()

此时sizeof( struct E )的值为7。

3 字节顺序

在将应用程序从一种架构类型迁移至另一种架构类型的过程中,经常会遇到字节排列顺序(endianness)问题。字节排列顺序是数据元素及其单个字节在内存中存储和表示时的顺序。有两类字节排列顺序:大端(big-endian)和小端(little-endian)。

对于 big-endian 处理器,如 POWER、PowerPC 和 SPARC,在将字节放到内存中时,是从最低位地址开始的,首先放入最重要的字节。另一方面,对于 little-endian 处理器,如 Intel 和 Alpha 处理器,首先放入的是最不重要的字节。像ARM处理器既有大端模式也有小端模式,在使用的时候要先确定模式。如图1所示说明了32位在大端和小端模式下的字节顺序。


如何获得一个平台的大端和小端信息,下面给出了使用指针方法的C描述:

int x = 1;

if ( *(char *) & x == 1)

printf( " little-endian \n" );

else

printf( " big-endian \n" );

出现字节顺序问题的原因是不一致的数据引用。它经常表现为数据元素转换使用联合数据结构或使用和操作位域导致数据类型不匹配。因此在进行操作的时候,要了解平台的字节顺序属性。

4 嵌入式Linux中代码移植实例

本节将通过一个基于移植编写的程序来复习移植性的问题。

数据对齐操作,依赖硬件平台。下面这个例子就和硬件平台有关系。

ssize_t ReadData( int fd, char * buf, size_t size)

{

int n;

int datalen;

n = readn(fd, buf, sizeof( int ) ); //读取数据

if( n <= 0) return n;

datalen = ntohl ( *((int *) buf )); //show error

if( datalen > size) return -2;

n = readn( fd, buf, datalen);

if( n > 0)

{

*(buf + n) = ‘ \0 ’;

}

return n;

}

在INTEL Xeon芯片的fedora 7运行正常。移植到Linux ARM开发板上运行,在show error处报告错误。根据报告的错误发现是总线错误,把错误定位于数据对齐方面,对代码进行排查。修改后的代码如下:

ssize_t ReadData( int fd, char * buf, size_t size)

{

int n;

int datalen, tmp;

n = readn(fd, buf, sizeof( int ) ); //读取数据

if( n <= 0) return n;

memcpy( &tmp, buf, sizeof( int ));

datalen = ntohl( tmp);

if( datalen > size) return -2;

n = readn( fd, buf, datalen);

if( n > 0)

{

*(buf + n) = ' \0 ';

}

return n;

}

再次进行编译。通过这个例子发现,在进行跨平台编写程序的时候,要十分细致。并对各种可能出现的问题进行排查。才能编写出适用平台移植的代码。


多数情况下,编写完全可移植的程序代码是不可能的。因为同样的数据类型在不同的编译环境下所产生的结果(OBJ代码)可能是不同的,特别是针对嵌入式系统,不同的运行平台可能要求不同的代码来实现它所要求的独特功能。为了增加程序代码可移植到多个平台的可行性,比较好的方法是提供一个可移植的数据或功能接口,让那些移植的部分隐藏在这些接口之后,当然,这样的事情应该全部是系统设计的工作。下面介绍有关可移植性编程的一些常规做法: 1、数据大小或长度相关性 C程序库所提供的“sizeof()”函数是一个很好的可移植的功能接口范例,对于不同的嵌入式系统的编译环境或平台,某些数据类型的大小或长度被解析成不一样的结果,而在程序体中,对这些数据类型的访问有十分严格的要求。所以在这种情况下,对这些数据类型的定义必须考虑到在不同环境的共享,也就是说,数据类型的定义将成为可移植的数据接口。例如,程序中可能有对8位、16位和32位的整数类型数据的访问的要求,为了增加程序代码的可移植性,惯常的做法是把这些整数以全局类型定义在某个H头文件中。例如: typedef signed char INT8; typedef unsigned char UINT8; typedef signed int INT16; typedef unsigned int UINT16; typedef signed long INT32; typedef unsigned long UINT32; 2、字节位序 不同的CPU,例如PowerPC和Inter X86系列,对于字节顺序的解析是完全相反的。也就是说,对于高字节在前面还是低字节在前面,它们的处理方法是截然不同的。这是又CPU内部寄存器的存储和访问机制决定的,也就是我们常说的大端模式和小端模式。这样的特点对程序中的字节和位操作将会有相当大的影响,所以可移植性编程应该将涉及位操作的程序设计成仅仅与固定的位序相关,变量或类型同样也定义成与CPU相关的数据接口,例如: typedef struct{ #if LittleEndian word hiword; word loword; #else word loword; word hiword; #endif } DWord; 3、位操作 在嵌入式系统开发中,基于存储空间的限制,我们经常利用位来表示某些设备或操作的状态,也就是说,位操作是一种使用十分频繁并高效的操作。同样位序也和CPU相关,所以习惯上将位的定位定义为一些宏,从而提高它们的可移植性。例如: #define BYTE_BIT0 0x01 #define BYTE_BIT1 0x02 #define BYTE_BIT2 0x04 #define BYTE_BIT3 0x08 #define BYTE_BIT4 0x10 #define BYTE_BIT5 0x20 #define BYTE_BIT6 0x40 #define BYTE_BIT7 0x80 4、对齐 对齐同样与CPU紧密相关,有些微处理器定义和要求严格的8位、16位或32位对齐,也就是说,对存储地址的访问或数据的读写必须以8位、16位或32位方式对齐,这样可能产生误操作,从而导致系统的不稳定或崩溃。因此,在可移植性编程中,应该对涉及此类操作的函数定义为可移植的接口函数。 那么 如何实现可移植性高的代码呢,我就先写几点吧,有些晚了,准备睡觉了


1.擅用define。请把“裸露”的常量,用短小又信息准确的宏定义起来,务必全大写。常量的宏定义要大写,我会在后期关于代码规范的主题文章分析。请把设备驱动的io,用define定义分离出来。当然,还有许多妙用,宏定义简直是移植旅行必备佳品。待我后期再整理下思路吧。


2.抽像出平台依赖严重的代码。比如访问一个特定mcu寄存器,开关中断,清狗指令,中断写法等等。


3.如果可以,我希望你的.c档中包含的.h档尽可能的少。这样在移植的时候,你只要看包含了那些.h档,你就知道该模块大概依赖了其他哪些模块。我知道,大多数的程序员都喜欢在.c档的文件头仅有一个

#include "includes.h"

而在includes.h 中包罗万象,这是原罪!当然,要一个萝卜一个坑地梳理清楚.c与.h档的关系,需要长期的工作经验,尤其在编译条件错综复杂时,操作起来的确痛苦且容易出错,但其实这已经预示了你系统架构的某种不合理。


如果没有足够的经验,那么我建议你,先在设备层的.c档尽量包含尽可能少的.h档。然后把设备层的.h档放在includes.h中,给应用层使用。


3. 打造自定义库,这个准备设专题讲解。


4. 通信数据统一是大端的,内部应用代码统一用数值说话,和大小端无关,不要乱糟糟的一片胡写蛮缠。少用union,发送数据时统一用单字节移位发出去,接收时用移位收进来。牺牲了效率,但提高了可移植性。在51中也许不大现实,但在未来M3的大趋势下,效率是可以牺牲的。


5.欲知后事如何,请听下回分解....