移植性問題包含的内容很廣泛,本節要介紹的是代碼在不同體系結構下的移植問題。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位在大端和小端模式下的位元組順序。

Big-endian
Little_endian
圖1 大端、小端中的位元組順序
如何獲得一個平台的大端和小端資訊,下面給出了使用指針方法的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.欲知後事如何,請聽下回分解....