【編譯器玄學(xué)研究報(bào)告】第一期——位域和volatile
【寫在前面的話】
【正文】
時(shí)間空出來(lái)了,我就可以做更多別的事情了唄……
時(shí)間空出來(lái)了,我好像沒(méi)別的事情做,那就……睡一會(huì)兒?jiǎn)h……
然而,我們廣大的可愛(ài)的朋友們用實(shí)際行動(dòng)告訴我們:
時(shí)間空出來(lái)了,我就托著腮看著外設(shè),直到它完成工作……唄……
//! 我故意不用STM32的例子,以防止更多的人受到冒犯
//! 一個(gè)串口發(fā)送單個(gè)字符的例子,這個(gè)代碼是我自己寫的
int stdout_putchar(char txchar)
{
CMSDK_UART0->DATA = (uint32_t)txchar;
while(CMSDK_UART0->STATE & CMSDK_UART_STATE_TXBF_Msk); //!托腮
return (int) txchar;
}
以上內(nèi)容扯遠(yuǎn)了……
外設(shè)是可以跟CPU同時(shí)工作的
外設(shè)寄存器的值在CPU沒(méi)有改寫的情況下是會(huì)被外設(shè)自己更新的
正因?yàn)槿绱耍x外設(shè)寄存器的時(shí)候要用volatile來(lái)修飾
接下來(lái),我再來(lái)介紹一些很多人一般不會(huì)注意到的事實(shí):
寄存器的訪問(wèn)是有對(duì)齊限制的
一個(gè)只支持WORD對(duì)齊訪問(wèn)的寄存器,如果你直接用Half-WORD的地址去訪問(wèn),比如訪問(wèn)一個(gè)4字節(jié)寄存器的高16位,你是很可能會(huì)觸發(fā)bus fault的
通常,大部分外設(shè)都支持多種訪問(wèn)對(duì)齊形式,比如WORD對(duì)齊、Half-WORD對(duì)齊和字節(jié)對(duì)齊,所以你不太會(huì)遇到這類問(wèn)題。但有些外設(shè)本身設(shè)計(jì)比較“樸素”——你可能就會(huì)遇到這類沒(méi)有蓋上蓋子的下水道。
寄存器的訪問(wèn)是有大小限制的
一個(gè)只支持以WORD大小訪問(wèn)的寄存器(只支持用volatile uint32_t *指針類型來(lái)訪問(wèn)的寄存器),哪怕你地址對(duì)齊了到了WORD,如果你用字節(jié)大小去訪問(wèn)(用volatile uint8_t *指針類型來(lái)訪問(wèn)),你也是很有可能會(huì)觸發(fā)bus fault的。
通常,大部分外設(shè)都支持多種大小的訪問(wèn),比如WORD大小的訪問(wèn)、Half-WORD大小的訪問(wèn)和字節(jié)大小的訪問(wèn),所以你不太會(huì)遇到這類問(wèn)題。但是,有些外設(shè)本身設(shè)計(jì)比較“樸素”——你可能就會(huì)遇到這類沒(méi)有蓋上蓋子的下水道。
NO,NO,NO,你太天真了。讓我們來(lái)看一個(gè)案例(同時(shí)為了防止人們對(duì)號(hào)入座,以下當(dāng)事人和代碼都已經(jīng)打碼)
typedef struct {
volatile uint32_t SEL : 8;
} example_reg_t
void set_selection_field(uint_fast8_t chSelection)
{
//! 使用位域來(lái)直接訪問(wèn) SEL[0:7]
EXAMPLE_REG.SEL = chSelection;
}
MOV r1,#0x40000000 ; 將地址值 0x40000000 存入r1
LDR r2,[r1,#0x00] ; 將 r1 當(dāng)作指針變量,讀取偏移量為0x00的一個(gè)word到r2中
BFI r2,r0,#0,#8 ; 將保存在r0中由用戶傳入的值提取低8位覆蓋r2的低8位
STR r2,[r1,#0x00] ; 將 r1 當(dāng)作指針變量,寫入r2中的WORD到目標(biāo)地址
BX lr ; 返回上一級(jí)函數(shù)
可見(jiàn),這里的代碼生成完全滿足我們的要求。當(dāng)我們移植同樣的代碼到LLVM或者基于LLVM的Arm Compiler 6下,神奇的一幕發(fā)生了:
注意,這里Arm Compiler 6使用了跟Arm Compiler 5一樣的優(yōu)化等級(jí)(-O1),可見(jiàn)原本的5條指令變成了3條,這里逐條解釋如下:
MOV r1,#0x40000000 ; 將地址值 0x40000000 存入r1
STRB r0,[r1,#0x00] ; 將 r1 當(dāng)作指針變量,寫入r0中的BYTE到目標(biāo)地址
BX lr ; 返回上一級(jí)函數(shù)
等一等?且不論之前的“讀改寫”被成功的“優(yōu)化掉了”(這個(gè)是沒(méi)有問(wèn)題的,因?yàn)樵镜募拇嫫鞫x中,我們就沒(méi)有給出剩下24bit的內(nèi)容,這等于告訴編譯器我們對(duì)這部分值是不在乎的,所以這里編譯器也沒(méi)有對(duì)剩下的24bit做“讀改寫”保護(hù)),
為什么uint32_t所明確標(biāo)記的word操作被替換成了byte操作??
我volatile白加了么?說(shuō)好的不會(huì)優(yōu)化呢?
編譯器你怎么不按套路出牌?
難道位域在Arm Compiler 6不能使用了么?——萬(wàn)一我的寄存器是只支持WORD大小訪問(wèn)的怎么辦?
這是編譯器的bug么?實(shí)錘了么?
Arm Compiler 6果然是垃圾么?果然還是armcc大法好!
先別急,我們?cè)賮?lái)看看定義本身:
typedef struct {
volatile uint32_t SEL : 8;
} example_reg_t
注意到?jīng)]有?這里volatile只覆蓋了位域SEL,也就是說(shuō)我們其實(shí)只告訴編譯器uint32_t中只有低8位是volatile的(只有一個(gè)字節(jié)是volatile的)——換句話說(shuō):“對(duì)uint32_t中的第一個(gè)字節(jié)的訪問(wèn)是不允許優(yōu)化的”,而其它部分我們沒(méi)有規(guī)定。這是不是意味著,LLVM和Arm Compiler 6編譯器特別較真,它覺(jué)得我們本意就是告訴它“要以byte的形式去訪問(wèn)一個(gè)uint32_t整形的第字節(jié)”呢?而且還“不允許優(yōu)化”。
為了驗(yàn)證這個(gè)想法,我們將剩下的部分補(bǔ)齊:
typedef struct {
volatile uint32_t SEL : 8;
volatile uint32_t : 24;
} example_reg_t
重新編譯工程,生成代碼如下:
果然,不僅讀改寫回來(lái)了,針對(duì)寄存器訪問(wèn)的大小也乖乖變回了uint32_t。
【玄學(xué)說(shuō)法】“Arm Compiler 6(armclang)比 Arm Compiler 5 不可靠、容易生成錯(cuò)誤的代碼”
【實(shí)際情況】Arm Compiler 6比Arm Compiler 5在語(yǔ)法理解上更嚴(yán)格,而Arm Compiler 5在語(yǔ)法理解上更寬松,并且隱含了一些編譯器自己的“私貨”,大家只不過(guò)是先入為主,早已習(xí)慣了armcc而已。
【后記】
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!