新聞中心

ARM平臺的地址對齊問題

作者: 時(shí)間:2016-11-09 來源:網(wǎng)絡(luò) 收藏
前言
ARM流行已久,做嵌入式開發(fā)的不知道ARM不大可能。鑒于其所具備的較低功耗下的較高性能,也就成了大多數(shù)嵌入式設(shè)備的首選了。
不過對于剛上手的人來說,有可能會遇到一些稀奇古怪的問題。畢竟大部分人都習(xí)慣了IA-32下的程序設(shè)計(jì),雖然兩者都是32位的處理器,但是體系架構(gòu)完全不同,于是也導(dǎo)致了一些隱含的問題。這里想描述一下一個(gè)有點(diǎn)蠱惑的問題,即在ARM上訪問非對齊地址內(nèi)容,會出現(xiàn)所謂“不可預(yù)料”結(jié)果的問題。
ARM內(nèi)存訪問的對齊問題
按照ARM文檔上的描述,其訪問規(guī)則如下:
1. 一次訪問4字節(jié)內(nèi)容,該內(nèi)容的起始地址必須是4字節(jié)對齊的位置上;
2. 一次訪問2字節(jié)內(nèi)容,該內(nèi)容的起始地址必須是2字節(jié)對齊的位置上;
(單字節(jié)的沒有這個(gè)問題,就不用考慮啦。 )
好,既然規(guī)則如此,那應(yīng)該遵守。不過么,不安分的人往往喜歡破壞規(guī)則,喜歡看看不遵守規(guī)則會有什么結(jié)果;另外么,即便遵規(guī)蹈距的人,有時(shí)也難免考慮不周,犯個(gè)錯(cuò)也是正常現(xiàn)象。好,那么讓我們來看看犯錯(cuò)的結(jié)果吧。例如下面的代碼:
char buff[8] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xab, 0xbc, 0xcd};
int v32, *p32;
short v16, *p16;
p32 = (int*)&( buff[1] ); //unalignment
p16 = (short*)&( buff[1] ); //unalignment
v32 = *p32; //what’s the result?
v16 = *p16; //what’s the result?
如果上面這段代碼在IA-32上運(yùn)行,那么結(jié)果應(yīng)該如下:
v32 = 0x9a785634
v16 = 0x5634
即便非對齊地址上訪問,IA-32也就是犧牲一點(diǎn)性能,但是結(jié)果保證是正確的。恩,這也是我們所期望的……
可是…… 換到ARM上呢?我們來看看在ADS1.2編譯后,執(zhí)行的結(jié)果如下:
v32 = 0x12785634
v16 = 0x1234
這個(gè)結(jié)果有點(diǎn)奇怪了吧。照理說指向0x34,那么如果是Big-Endian的話,v32應(yīng)該是0x3456789a,如果是Little-Endian的話,就是前面IA-32的結(jié)果??涩F(xiàn)在的結(jié)果呢?兩者都不是,莫名地把更低地址的0x12給湊進(jìn)來了…… 而如果看看編譯生成的匯編code的話,這兩個(gè)賦值很簡單,分別用了ldr和ldrsh指令,指令沒有問題,分別用于讀取32位和16位數(shù)據(jù),都是最基本的指令。嗯,嗯,這就是我們所要描述的訪問非對齊地址的問題了。
問題的緣由(個(gè)人猜測,非官方資料……)
個(gè)人感覺呢,這是ARM體系架構(gòu)實(shí)現(xiàn)的問題,或者說這本來就是By Design的。這樣做簡化了處理器的實(shí)現(xiàn),IA-32實(shí)現(xiàn)的時(shí)候肯定會對讀取地址是否對齊進(jìn)行判斷,然后轉(zhuǎn)換為相應(yīng)的操作 ,而ARM呢?沒有做這個(gè)事情,默認(rèn)認(rèn)為大家都按照規(guī)矩辦事,你要是膽敢破壞,俺就給你好看~~~
那有沒有辦法解決呢?
這個(gè)問題其實(shí)ARM自己也知道,所以呢,它在編譯器里面,已經(jīng)添加了部分支持。不過有人會問,那上面那個(gè)情況呢?為什么結(jié)果還是不對呢?好像沒有添加什么支持嘛……
嗯,其實(shí)ARM是做了一定的努力的,只是這個(gè)情況它沒辦法解決…… 它做的事情就是:在編譯器能夠的得知的情況下,盡量保證訪問內(nèi)容的正確。這句話有點(diǎn)籠統(tǒng),那么把具體情況一個(gè)個(gè)來看看吧。
編譯器的努力(1)——所有局部/全局/靜態(tài)等變量都放在4字節(jié)對齊的地址上
其實(shí)這個(gè)努力很常見,由于在32位平臺上,一次訪問4字節(jié)是效率最高的,所以大多數(shù)32平臺的編譯器都如此處理,ARM的ADS也不例外。
編譯器的努力(2)——填充、填充、再填充
這個(gè)事情么,其實(shí)也是常見的。各類編譯器上,對于某些結(jié)構(gòu)定義中會產(chǎn)生不對齊的情況,自動(dòng)填充,以提高訪問效率(例如IA-32上訪問非對齊的,會加1個(gè)周期的)。而ARM的編譯器也一樣操作,不過感覺這里不單單是為了提高效率,也能夠順帶解決這個(gè)不對齊的問題。
編譯器的努力(3)——產(chǎn)生特殊代碼
嗯,這個(gè)就是關(guān)鍵了,也是ARM編譯器的與眾不同之處。先來看一段代碼:
__packed typedef struct _test
{
char a;
short c;
int d;
} test;
char buff[8] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xab, 0xbc, 0xcd};
test *p = (test *)buff;
v32 = p->d; //這里的v32借用上面的定義;
貌似多了個(gè)限定為__packed的struct,以此來造成不對齊的狀況,看不出多大區(qū)別嘛。可是運(yùn)行一下的話,就會發(fā)現(xiàn)這里的結(jié)果是正確的。我們來看看ADS生成的匯編代碼吧。
v32 = q->d;
[0xe2890003] add r0,r9,#3
[0xeb000088] bl __rt_uread4
[0xe1a05000] mov r5,r0
看到這里的那條"bl __rt_uread4"的指令了吧。對ARM指令有一定了解的都知道bl其實(shí)就是一個(gè)函數(shù)調(diào)用。所以,這里的代碼其實(shí)是調(diào)用了ADS自己提供的__rt_uread4函數(shù),該函數(shù)完成的操作就是讀取四個(gè)字節(jié)。ADS提供了類似的一系列函數(shù),針對signed/unsigned,以及4字節(jié)/2字節(jié)的讀取/寫入操作。
估計(jì)看到這里,大家會問,如果沒有__packed限定符呢?猜對了,沒有__packed限定符,那么編譯器會對上面的情況pending,所以這個(gè)struct里面的d所在的位置是4字節(jié)對齊的(編譯期信息,而非實(shí)際運(yùn)行期信息)。所以就回到類似最初的例子了。
那么,還有一種情況,就是在有__packed的情況下,而struct里的字段都是符合對齊要求的,那么生成的代碼會是怎么樣的呢?從實(shí)際生成的代碼來看,和上面的這段匯編代碼,唯一的區(qū)別就是第一條指令把#3改成了#4,而后面仍舊調(diào)用__rt_uread4函數(shù)。嗯,這樣結(jié)論就出來了:
編譯器會在使用__packed的情況下,自動(dòng)對其中的4字節(jié)/2字節(jié)訪問添加特殊代碼,以保證其結(jié)果的正確。
好了,這個(gè)關(guān)于這個(gè)問題描述得差不多了,可能的話,盡量倚賴編譯器的這些功能,而對于編譯器無能為力的部分,就要靠萬分小心了……
p.s. 其實(shí)這里有很多事情可以來盡量預(yù)防此類問題,比如嵌入式項(xiàng)目往往喜歡自己管理內(nèi)存分配,那么自己寫的內(nèi)存分配函數(shù)就保證返回的地址都是4字節(jié)對齊位置上的……


關(guān)鍵詞: ARM平臺地址對

評論


技術(shù)專區(qū)

關(guān)閉