
大家在平常的編程過程應該會碰到各種奇葩的問題吧,反正我最近是碰到了一次,再此跟大家分享一下。事情的原因是我在程序中增加了一個變量,然后就會導致程序每次都會進入異常。
我將代碼簡化了,使用兩個模塊來演示這個問題。第一個模塊是Dev模塊。
下面是Dev模塊的頭文件,Dev結構體中有一個數組。
#include?
#include?
typedef?struct?{
?int?a_[100];
?char?b_;
}Dev;
void?DevInit(Dev?*c_this);
下面是Dev模塊的源文件代碼,里面只有一個memset。
#include?"Dev.h"
void?DevInit(Dev?*c_this)?{??
?memset(c_this,?0,?sizeof(Dev));
}
第二個模塊是DevManager,相關數據結構如下:
#include?"Dev.h"
#pragma?pack(1)
typedef?struct?{
?Dev?uart_;
?char?num_;?//?Adding?this?variable?causes?a?crash
?Dev?iic_;
}DevManage;
#pragma?pack()
DevManage?dev_manage;
void?DevManagerInit(void)?{
?DevManage?*c_this?=?&dev_manage;
?
?memset(c_this,?0,?sizeof(DevManage));
?
?DevInit(&c_this->uart_);
?DevInit(&c_this->iic_);
}
DevManage結構體包含uart以及iic設備,以及我新加入的一個num_變量,由于新增了num_變量以及與之相關的業務會導致每次調試目標板都會進入異常。
根據調試情況看,每次都會出現異常,說明是個小問題。就怕偶爾出現異常,不容易復現。
思路應該非常清晰,出現異常時候查看LR寄存器的值,LR寄存器主要有兩個功能。
根據LR寄存器的值,從而定位到是執行DevInit(&c_this->iic_)函數中的memset導致的。
第一反應是DevInit中傳入的對象可能為空,操作了非法內存才導致的錯誤。于是又重新調試了一遍,發現DevInit中對象的地址并不為空,而且就是等于實體對象中設備的地址。
這一刻我陷入了深深的自我懷疑,memset難道不是這樣用的?難道不是傳入一個地址,清0,然后sizeof(DevManage)就完事了?
我這代碼怎么會出錯,memset就是這樣用的,天王老子來我也是對的,這樣的心理是不是也深度還原了碰到問題時的你們。
如果是你,該如何繼續...
有人說,匯編是最后的救命稻草。那我也嘗試抓住這根稻草,出現問題時如下圖:

問題主要出現紅色箭頭指向的這一行,經過查詢資料得知_aeabi_memclr4是一個用于ARM嵌入式系統的函數,用于將內存區域清零。函數名中的"_a"表示該函數符合ARM嵌入式應用二進制接口(Embedded Application Binary Interface,EABI)規范。在調用_aeabi_memclr4時,需要確保傳入的內存地址是四字節對齊的。再看圖片中的對象地址為0x200001BD,剛好比四字節對齊地址0x200001BC多了一個字節。
再回頭看DevManage對象,這里使用了偽指令#pragma pack(1)讓內存分配進行單字節對齊。因為Dev對象是按照四字節對齊的,緊接著引入了新的成員num_占用一個字節。所以導致iic_對象的地址相對于未增加變量之前的地址偏移了一個字節,導致不是四字節對齊的了,從而引發了錯誤。
就示例中的代碼而言,只需要把強制DevManage對象單字節對齊的功能刪除即可解決問題,因為默認情況下是四字節對齊的。
由于#pragma pack(1)具有作用域限制,這里其實只是對num_變量做了單字節對齊的限制,而并沒有對Dev對象的內存分配起到限制作用,Dev對象仍然是按照4字節對齊的(默認值),所以Dev對象的所占用的長度一定是101*4=404字節。而整個DevManager對象的大小就101 * 4 + 1 + 101 * 4 = 809字節,成員分配如下圖所示。

到最后在拋出一個問題,對于上述的代碼在vscode中使用gcc編譯執行為何沒有問題?
END
點贊、轉發加關注,一鍵三連,好運年年
關注公眾號后臺回復數字688可獲取嵌入式相關資料
往期推薦
關于海明碼,我悟了

為什么C語言執行效率高,運行快?

工作這么久,才明白的SOLID設計原則

Keil斷點進階用法,不看后悔

