Ivan’s Blog


  • 首頁

  • 歸檔

  • 標籤

[Linux] - cppcheck 靜態程式分析工具

發表於 2016-07-01   |  

cppcheck是一套跨平台的靜態程式分析工具(static code analysis tool),主要用來分析C語言在coding上的問題,包括memory leak、null pointer、buffer overflow這些算是bug的問題,也可檢查出多餘的程式碼達到增進程式效能的目的,例如沒使用到的變數、給定初始值的參數但從未使用過此初始值、不可能進入的condition等等

使用cppcheck

首先必須安裝cppcheck

sudo apt-get install cppcheck

使用時輸入以下指令,最後的enable=all代表所有的檢查都執行,包含coding上的問題、邏輯上的問題、效能上的增進等等

cppcheck xxx.c --enable=all

因cppcheck default會作許多preprocessor的檢查,這部分會耗費相當長的時間,若不需要可將其關閉

cppcheck -DA xxx.c --enable=all

接著我們看一下一些都可編譯過但事實上是有問題的例子,看cppcheck是否可檢查出來

變數宣告

觀察下段程式碼,我們來看一下cppcheck可檢查出哪些問題

1
2
3
4
5
6
7
8
int a;
int b;
int c = 1;
int d = 2;

b++;
c++;
d = 4;

(style) Variable 'd' is reassigned a value before the old one has been used.

d被初始化成2但是還沒使用過前就被改掉了,因此賦予初始值是多餘的動作

(style) Unused variable: a

a被宣告但從未使用過,此宣告可以直接拿掉

(style) Variable 'b' is not assigned a value.

b需要給初始值,因在後面的程式有作b++,若不給予初始值會是未定義的數值+1(local變數在ANSI C裡並沒有規定default值需為0,事實上對local variable賦予初始值對CPU來說也是一項負擔)

(style) Variable 'd' is assigned a value that is never used.

d被賦予初始值,但從未使用過

(error) Uninitialized variable: b

和第三項相呼應,作b++但未給予初始值

記憶體管理

記憶體配置在寫程式上是非常好用的技巧,可用精簡的方式完成許多複雜的事情,但在C的架構上部分的記憶體管理需由程式設計師自行負責,若使用不當容易造成run time error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int *ptra;
int *ptrb;
int *ptrc;
int *ptrd = NULL;

ptra = (int*)malloc(100);
ptrb = (int*)malloc(100);

if (NULL == ptra) {
return *ptrd;
}

*(ptrc + 1) = 5;

free(ptrb);

*(ptrb + 2) = 6;

(error) Null pointer dereference: ptrd

ptrd在程式執行過程中有回傳值,但卻沒有配置參考的位址

(style) Variable 'ptrc' is not assigned a value.

ptrc過程中有被指定數值,但卻沒有配置參考的位址

(error) Memory leak: ptrb
(error) Memory leak: ptra

ptra有malloc卻沒有free,ptrb最後有free,但在if(NULL == ptra)裡面沒有free,也有機會造成memory leak

(error) Dereferencing 'ptrb' after it is deallocated / released

ptrb在free之後還對其操作

(error) Null pointer dereference

和第一項相呼應,回傳位配置空間的位址的值

(error) Uninitialized variable: ptrc

和第二項相呼應,ptrc沒有配置空間

Buffer邊界

C的架構上對於buffer的overwrite是不會產生任何錯誤或警告訊息的

1
2
3
4
5
int i;
int a[10];

for (i = 0; i <= 10; i++)
a[i] = i;

(error) Array 'a[10]' accessed at index 10, which is out of bounds.

迴圈造成記憶體被overwrite

目前市面上的靜態程式檢查工具眾多,例如splint, coverity, cppcheck等等,每個工具都有其擅長的部分
就使用過splint和cppcheck的經驗來看例如對於不需要使用或不需要給初始值的狀況cppcheck > splint,但對於buffer overwrite的檢查splint > cppchek,因此若想讓程式更加完善,可以使用多些檢查工具達成目標

最後分享一下書上看到的

Lint Early, Lint Often

Lint is your software conscience. It tells you when you are doing bad things. Always use lint. Listen to your conscience.

[Atmel][Sama5d3-xp] - 簡介Bootstrap

發表於 2016-07-01   |  

AT91Bootstrap是bootloader的第二階段,緊接在ROM boot之後,主要負責硬體的初始化及配置,例如clock PIO DRAM等等的配置,以及複製Flash or SD card的uboot到RAM上

ROM boot流程

板子一上電即開始執行ROM code,首先會先確認NAND chip select(JP5)的配置
若為1(short)則會開始從SPI CS0 flash、SD card、NAND flash、SPI CS1 flash、EEPROM照順序尋找是否有可使用的bootstrap(註1),若有的話則讀取一份到SRAM且跳過去,若沒有任何可使用的bootstrap則進入SAM-BA監控模式
若為0(open)則會直接進入SAM-BA監控模式
進入SAM-BA模式則可藉由相關工具將程式燒到flash or EEPRM

註1: sama5d3-xplained僅支援nand flash或sd card boot

前置作業

要build在開發版上跑的程式必須先有cross compiler,而sama5d3是ARM Cortex-A5的CPU,因此用的gcc如下

sudo apt-get install gcc-arm-linux-gnueabi

安裝完cross compiler之後必須下載bootstrap的src code(當然必須先有git相關指令,這部份請自行安裝)

git clone git://github.com/linux4sam/at91bootstrap.git

下載完成後進入at91bootstrap資料夾,即可使用make相關指令產生.config檔以及二進位檔

註2: 也可建立完整的toolchain來得到cross-compile gcc

建置程式

因為Sama5d3預設本來就是跑Linux的,因此建置at91bootstrap過程和Linux的bootstrap/uboot基本上沒什麼太大的不同
你可以在抓下來的程式碼當中的board/sama5d3_xplained資料夾內發現四個default configuration files,如下

sama5d3_xplainednf_linux_image_dt_defconfig
sama5d3_xplainednf_uboot_defconfig
sama5d3_xplainedsd_linux_image_dt_defconfig
sama5d3_xplainedsd_uboot_defconfig

nf代表從nand flash讀取,sd代表從sd card讀取
linux代表讀取linux kerenel到RAM,dt代表讀取dtb到RAM,uboot代表讀取uboot到RAM

要使用怎樣的configuration file需看需求決定,不過從檔案當中可以發現也可以選擇不讀取uboot直接讀取kernel的方式帶起linux
一般我們都使用flash且帶uboot的方式,在最上層的資料夾裡輸入以下make指令可產生.config檔

make mrproper
make sama5d3_xplainednf_uboot_defconfig

另外也可以使用以下指令進行產生客製化的.config檔,細部使用不在此篇敘述

make menuconfig

最後輸入以下指令,會在最上層目錄中產生一個binaries資料夾,裡面包含了bin檔及elf檔,將bin檔放置flash當中的bootstrap位置即可在ROM code執行完成後,跳到此段程式執行

make CROSS_COMPILE=arm-linux-gnueabi-

燒錄到nandflash

建置完bootstrap之後可看到最上層目錄多出了一個binaries資料夾,裡面的sama5d3_xplained-nandflashboot-uboot-3.8.5.bin即是我們要燒錄到flah的檔案

我們可以到atmel官網下載SAM-BA工具,檔名為SAM-BA 2.16 for Linux,下載完之後解壓縮可得到一sam-ba_cdc_linux資料夾,裡面的執行檔(sam-ba or sam-ba_64)即是我們要使用的工具

首先先把NAND chip select(JP5)設為0接著RESET,若從UART看到RomBoot的log代表成功進入sam-ba監控模式,接著必須把JP5再設回1

接下來要啟動sam-ba,正常打開應可看到Select connection有相關資訊,若沒有的話可能是權限問題,可以用sudo開啟此檔案

1
sudo ./sam-ba

開啟後畫面如下,將板子選至at91sama5d3x-xplained後按Connect

將標籤頁選至NandFlash,Scripts選擇Enable NandFlash,之後按Execute,沒看到紅色字代表成功

現在要把bootstrap燒到nandflash裡了,將Scripts選至Send Boot File,之後按Execute再選擇bin檔,一樣沒看到紅色字代表成功

現在bootstrap已經燒好了,可以將sam-ba關掉,且將板子RESET即可看到以下畫面代表成功執行bootstrap了

[Atmel][Sama5d3-xp] - Boot sequence

發表於 2016-06-30   |  

Sama5d3的boot sequence主要分為三個階段

  • ROM code
  • AT91Bootstrap
  • U-Boot

Boot

開機的流程如下圖

  1. ROM code先確認目前是由何種方式boot(Flash or SD card),若選定的存取裝置中存在AT91Bootstrap,則ROM code會負責將此段二進位檔讀取一份到SRAM,接著跳過去
  2. AT91Bootstrap負責相關的硬體配置,另外也必須將存取裝置中的U-Boot/barebox讀取一份到SDRAM/DDRAM,接著跳過去
  3. U-Boot負責將Kernel從儲存裝置讀取一份到SDRAM/DDRAM,之後即開始執行Kernel

一般Kernel會是Linux kernel,但也可以在bootstrap設定好相關硬體後載入其它OS,甚至沒有OS直接進入一無窮的while,照順序性的執行任務

也可以在Bootstrap時只設定需使用到的硬體,接著直接執行相關任務,也就是說從ROM code跳到RAM之後就開始執行任務

當然還有各式各樣的不同應用,使用者只要知道當ROM code結束之後會跳到SRAM開始執行程式,其它端看需求使用

Memory map

下圖為Sama5d3的儲存裝置配置圖,有了這張圖即可得知需把各段的程式碼放置何處,例如AT91Bootstrap就要放在offset 0x000000的地方,而U-Boot則需放在0x040000

[C語言] - gets()設計缺陷

發表於 2016-06-29   |  

gets()設計是用來取得輸入的字串。但此function有重大的缺陷,即不會去檢查buffer是否足夠使用

Bug

先看以下這段程式碼

1
2
3
4
5
6
7
int main(void)
{
char line[512];
...
...
gets(line);
}

程式會將取得的字串放進line這個buffer,但gets()不會去確認buffer是否足夠,實際上也無法確認,因gets()無法得知buffer大小的資訊,如果輸入的字串大於buffer,那gets()會將此字串完整的貼到buffer裡,即便會超過此buffer造成其他的memory被覆蓋掉

Internet worm

這個簡單的bug卻可以造成嚴重的危害。正常來說local variable的buffer會放在stack裡面,某些重要資訊也可能放在stack,例如return address(RA),若剛好遇到此架構的RA是放在stack,那惡意的使用者就可以輸入一個非常長的字串,讓gets()蓋掉其它stack的資訊,包括了RA,當此function執行完之後準備jump回原本的caller function,但RA已經被改成惡意使用者的function address,即可破壞甚至取得此架構的控制權

Patch

還好已經有可替代的function避免這個問題,就是改使用fgets(),fgets()需帶入buffer大小的參數,因此會幫忙檢查是否造成overwrite,在Linux下輸入man gets可得到相關資訊,其中最後的部分寫到SECURITY CONSIDERATIONS即為相關說明

[Atmel][Sama5d3-xp] - Getting Started

發表於 2016-06-17   |  

在開始開發之前必須先有相關的工具,首先你必須有一條USB Micro-A轉USB Type A的傳輸線,用來連接主機端和板端(J6)

USB傳輸介面主要是用來提供板端5V DC,也可以輸出log,但強烈建議使用UART(J23 Debug port),來進行相關訊息的輸入與輸出,某些較底層的資訊只會從UART輸出

要使用UART最方便的方式就是準備一個RS232轉USB的裝置,一邊連接到J23一邊連接到主機端,之後藉由終端機來輸出或輸入訊息,FTDI作了許多相關的產品,市面上可以很輕易的取得

終端機

在Linux或Windows作業系統下都有終端機可用來接受裝置端的訊息,Windows下較常見的工具為putty,但比較建議還是在Linux下開發,會有較佳的彈性和支援的工具

Linux下用來輸出訊息的工具就很多了,例如picocom screen minicom等等,這邊使用picocom作為範例

首先當然要先安裝picocom

sudo apt-get install picocom

安裝完之後將UART板端配置好之後接到主機端的USB接口,可由以下指令知道是否有連接成功,此例可看出成功連接到ttyUSB0

dmesg | grep tty

接著使用picocom接收UART訊息且指定ttyUSB0且baud rate為115200,若無法成功使用可加sudo增加權限

sudo picocom -b 115200 /dev/ttyUSB0

最後就可以將micro USB(J6)接到主機端即可看到板端輸出的訊息

[Atmel][Sama5d3-xp] - 簡介

發表於 2016-06-16   |  

Atmel開發的Sama5d3 Xplained是使用ARM Cortex-A5處理器的開發版,整合了眾多的週邊裝置,而且和Arduino完全相容,因此可以使用市面上許多為Arduino所開發的模組

裝置清單

主要週邊裝置以Cortex-A5為基礎,搭配兩個Ethernet,兩個SD/MMC/SDIO host,兩個usb host,兩個usb device,一個RGB LCD介面,一個JTAG介面,一個UART介面,詳細清單及外觀如下

處理器

使用擁有高效能且低功耗的Cortex-A5處理器,整合廣泛使用的周邊裝置。Atmel ARM Cortex-A5也支援DDR2和NAND flash儲存裝置,用來存二進位檔和一般資料,系統跑在166MHz的多階層bus架構,24組DMA通道,以及2塊64KB SRAM,下圖為Sama5d3的Block diagram(詳細資料請參考Atmel ARM Cortex-A5的datasheet)

[C語言] - 執行順序探討

發表於 2016-06-02   |  

C語言在作算術運算時會遵守先乘除後加減後加減的基本規則,而各種運算元也都有相對應的優先權(某些運算元的優先權並不符合直覺,因此對於不熟悉的運算盡量加大括號來限制運算順序)

若非一般的運算,某些執行順序是屬於未定義的(undefined)

賦值順序

1
2
3
int a;
int b = 1, c = 2;
a = b = c;

此段程式只有賦值運算,因此無法從優先權判斷優先順序,必須從關聯性(Associativity)判斷,而賦值是右關聯性(right associativity),也就是c會先賦值給b再賦值給a,因此a最終為2

為了符合正常使用,一般的 + - * / 運算皆為左關聯性(left associativity)運算,但像上述例子又是右關聯性運算,容易造成混淆,較好的作法則是將非一般使用的情況可以分開寫或加上大括號,以避免因為錯誤認知關聯性造成運算結果不如預期,例如上例應改為b = c; a = b;較佳

函式順序

1
x = f() + g() * h();

此運算會將g()和h()的回傳值相乘之後再加上f()的回傳值。但是g()和h()的執行順序則是未定義,可能g()先執行之後再執行h(),也可能h()先執行之後再執行g(),編譯器可根據狀況任意決定執行順序,若是這兩個函式皆會更改某一個全域變數,且根據此全域變數來決定回傳值的話,那麼執行順序不同會得到不同的結果

函式參數順序

1
2
3
4
5
int a[] = {1, 2, 3};
int *pa;

pa = &a[0];
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa), *(pa++),*(++pa));

此段程式碼的 *(pa), *(pa++), *(++pa)的執行順序是未定義的,因此在不同的編譯器上有可能得到不同的結果,我們在同一部x86機器上先用VS2010編譯且執行得到以下結果

接著以同樣的機器在ubutu下使用gcc編譯可得到以下結果

由此可見非定義行為將會由compiler決定,大部分的未定義行為可視為coding上的錯誤

[C語言] - char** 和 const char** 賦值問題

發表於 2016-05-31   |  

在討論賦值問題以前,我們先看ANSI C標準6.3.16.1,包含了以下限制

  1. 兩個操作都是指向有修飾符或無修飾符相容類型的指標
  2. 左邊指標指向類型必須擁有右邊指標指向類型的所有修飾符

char* 和 const char* 賦值

接著我們看一下下面這段程式碼

1
2
3
4
char *cp;
const char *ccp;
ccp = cp;
cp = ccp;
  • char* 型別為指到char型別的指標,且由const修飾
  • const char* 型別為指到char型別的指標,沒有修飾符

使用gcc編譯後會得到下面訊息,為什麼cp = ccp會有warning?原因如下

  • ccp和cp皆為指到char型別的指標,只是ccp有const修飾符,符合上述ANSI C標準第一項
  • ccp = cp左操作(const)擁有右操作(none)所有修飾符,符合上述標準第二項
  • cp = ccp左操作(none)沒有擁有右操作(const)所有修飾符,違反上述標準第二項

事實上這樣的限制也蠻好理解的,若是cp = ccp成立的話,那我們就可以用任意的指標修改一個經由const修飾過後的指標的值

char** 和 const char** 賦值

接著我們看以下這段程式碼

1
2
3
char **cp;
const char **ccp;
ccp = cp;

使用gcc編譯後會得到下面訊息,為什麼多了一層的指標就無法順利編譯完成了呢?

根據ANSI C標準6.1.2.5

  • char** 型別為指向char型別指標的指標,沒有修飾符
  • const char** 為指向有const修飾符指標的指標,沒有修飾符

兩者指向的型別不一樣,違反了上述標準的第一項

Note : 部分編譯器並不遵守此項規範,但為了程式的可移植性,還是盡量避免這樣的用法

[C語言] - switch使用探討

發表於 2016-05-25   |  

switch為C提供的條件判斷式,只能用來比較數值或字元。ANSI C標準規定switch裡面的case至少需支援257個成員,因為字元長度為8-bit (256個可用字元 + EOF)

貫穿 (Fall Through)

貫穿(Fall Through)指的是當switch進到特定的case中執行完動作後並不會自動break,執行流程會繼續往下跑直到看到break聲明。某些場合我們會使用貫穿的特性讓程式較精簡,但使用此特性的場合並不多,因此仍然必須仔細確認程式流程

1
2
3
4
5
6
switch (2) {
case 1: printf("case 1\n");
case 2: printf("case 2\n");
case 3: printf("case 3\n");
default: printf("default\n");
}

此段程式碼結果如下,由case2貫穿到default

Break

Break除了使用在switch來跳出外,也可用在迴圈中的跳出,也因此可能造成非預期的流程但編譯器無法檢查出來

此段程式在if (y == OTHER_STUFF)後接了break(顯然是coding上有錯誤),原本是預期break之後可以執行initialize_modes_pointer();但因為此break在case2裡,且也不在迴圈裡面,因此會直接跳離此switch判斷式造成非預期的行為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch (2) {
case 1:
doit1();
break;
case 2:
if (x == STUFF) {
do_first_stuff();
if (y == OTHER_STUFF)
break;
do_later_stuff();
} /* coder meant to break to here... */
initialize_modes_pointer();
break;
default:
processing();
}

Default

當所有的case都不滿足時會進入default條件裡面(如果有的話),通常我們習慣將default放在最後一個判斷,但其實default是可以放在任何位置的

1
2
3
4
5
6
7
8
switch (4) {
case 1: printf("case 1\n");
break;
default: printf("default\n");
case 2: printf("case 2\n");
break;
case 3: printf("case 3\n");
}

此段程式,然仍能夠正確的達到預期的結果

這時衍伸出一個有趣的問題,如果說default可放在任意位置,那是否代表switch在CPU執行時的行為和if…else並不相同,而是比較接近於goto的使用呢?

我們在x86的機器上將以上的程式碼編譯成組合語言

mov    DWORD PTR tv64[ebp], 4

將數字4移到ebp

cmp    DWORD PTR tv64[ebp], 1

將數字1和ebp比較

je    SHORT $LN4@main

若相等則跳到LN4


因為ebp皆不等於1,2,3因此最終會跳到LN3,另外default裡沒有break,從組合語言可以看到最終沒有接jmp,因此會繼續往下跑執行case2最終執行jmp到LN5

由此可見switch是由建立多個label+jmp到特地位置,而if…else則是經過不斷的cmp+jmp,兩者並不相同

12
Ivan Lin

Ivan Lin

Ivna’s Blog

19 文章
4 標籤
© 2017 Ivan Lin
由 Hexo 設計
主題 - NexT.mist