1. 程式人生 > >C語言版——點亮LED燈,深入到棧

C語言版——點亮LED燈,深入到棧

nand啟動 等於 halt 過程 畫出 示意圖 color 常見 defined

在上一篇進行了匯編語言的編寫之後,我們采用C語言來編寫程序,畢竟C語言才是我們使用最多的語言。

僅僅是點亮LED燈顯然太過於簡單,我們需要分析最後的反匯編,了解函數調用棧,深入C語言骨髓去分析代碼,並且自己編寫C語言的庫函數版本,方便以後開發,同時也是對自己C語言封裝能力的鍛煉。

先貼韋老大的代碼:

start.s:

.text
.global _start

_start:

    /* 設置內存: sp 棧 */
    ldr sp, =4096  /* nand啟動 */
//    ldr sp, =0x40000000+4096  /* nor啟動 */

    /* 調用main */
    bl main

halt:
    b halt

LED.c:

int main()
{
    unsigned int *pGPFCON = (unsigned int *)0x56000050;
    unsigned int *pGPFDAT = (unsigned int *)0x56000054;
    /* 配置GPF4為輸出引腳 */
    *pGPFCON = 0x100;
    
    /* 設置GPF4輸出0 */
    *pGPFDAT = 0;
        
    return 0;
}

C語言操作,的傳統IDE開發當中,我們只用從main函數開始寫代碼就行了,但是IDE隱藏了太多技術細節。

我們在arm嵌入式linux開發過程中,都需要自己來,這對初學者是不友好,但是對深入學習卻是很有幫助的。

首先,第一點,nand flash啟動,使用片內4k sram。

我們都知道,函數調用和局部變量的存儲需要使用到一種叫做棧的數據結構。

這裏說明,s3c2440,采用默認的棧生長方式,這也是我們最常見的方式,高地址往低地址生長。

要調用main函數,我們需要開辟棧,這裏使用片內4k 內存作為棧。

看看反匯編:

led.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0da01     mov    sp, #4096    ; 0x1000
   4:    eb000000     bl    c <main>

00000008 <halt>: 8: eafffffe b 8 <halt> 0000000c <main>: c: e1a0c00d mov ip, sp 10: e92dd800 stmdb sp!, {fp, ip, lr, pc} 14: e24cb004 sub fp, ip, #4 ; 0x4 18: e24dd008 sub sp, sp, #8 ; 0x8 1c: e3a03456 mov r3, #1442840576 ; 0x56000000 20: e2833050 add r3, r3, #80 ; 0x50 24: e50b3010 str r3, [fp, #-16] 28: e3a03456 mov r3, #1442840576 ; 0x56000000 2c: e2833054 add r3, r3, #84 ; 0x54 30: e50b3014 str r3, [fp, #-20] 34: e51b2010 ldr r2, [fp, #-16] 38: e3a03c01 mov r3, #256 ; 0x100 3c: e5823000 str r3, [r2] 40: e51b2014 ldr r2, [fp, #-20] 44: e3a03000 mov r3, #0 ; 0x0 48: e5823000 str r3, [r2] 4c: e3a03000 mov r3, #0 ; 0x0 50: e1a00003 mov r0, r3 54: e24bd00c sub sp, fp, #12 ; 0xc 58: e89da800 ldmia sp, {fp, sp, pc} Disassembly of section .comment: 00000000 <.comment>: 0: 43434700 cmpmi r3, #0 ; 0x0 4: 4728203a undefined 8: 2029554e eorcs r5, r9, lr, asr #10 c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1} 10: Address 0x10 is out of bounds.

說明:

<.comment>:在上面的反匯編當中,它不是匯編代碼的一部分,是註解,給我們一些提示,便於我們閱讀、理解的。在講解這個匯編之前,我們需要先看一個arm寄存器的別名表。

技術分享

好,現在開始分析,其中最重要的也是新出現的兩個指令:

stmdb,ldmia

詳細介紹可以參見韋老大書籍P53,arm嵌入式系統開發P58。

簡單說明,db表示事先遞減方式,ia表示事後遞增方式。

先對反匯編進行註解:

  led.elf:     file format elf32-littlearm
  
  Disassembly of section .text:
  
  00000000 <_start>:
    0:    e3a0da01     mov    sp, #4096    ; 賦值4096給sp堆棧寄存器
    4:    eb000000     bl    c <main>    ;跳轉到main函數,同時保存main函數返回地址到lr寄存器,返回地址為下一條指令的地址,這裏lr應為8
  
  00000008 <halt>:
   8:    eafffffe     b    8 <halt>;死循環
 
 0000000c <main>:
    c:    e1a0c00d     mov    ip, sp;備份sp寄存器的值到ip,ip是r12寄存器的別名
  10:    e92dd800     stmdb    sp!, {fp, ip, lr, pc}
; 這條指令需要重點講解,首先,fp,ip,lr,pc分別對應寄存器:r11,r12,r14,r15
; stm指令多寄存器操作的時候,後面的寄存器從編號高的先開始存儲,把後面幾個寄存器的值寫入sp所對應的內存塊中,(註意和但寄存器操作區分開來)後綴db表示 先減後存儲
; sp後面加了一個感嘆號!,表示sp最後的值等於最後被修改的值,不加感嘆號就算操作使sp更改過,sp也等於最初的值
; 首先操作r15,即pc,sp先減,sp=sp-4,此時sp=4092,pc等於當前指令地址加8,即此時pc=0x10+8=0x18,相當於把0x18寫入sram地址4092
; 同理操作r14,即lr,sp=sp-4,lr=8,相當於把0x8寫入4088地址
; r12,ip是等於4096的,上面的move指令,r11,fp的值此時未定
  14:    e24cb004     sub    fp, ip, #4    ; fp=ip-4=4092
  18:    e24dd008     sub    sp, sp, #8    ; sp=sp-8=4072
  1c:    e3a03456     mov    r3, #1442840576    ;#1442840576等於十六機制0x56000000,把這個數存放在r3中
  20:    e2833050     add    r3, r3, #80    ;r3=r3+80=0x56000050
  24:    e50b3010     str    r3, [fp, #-16];fp-16=4076,把0x56000050放入內存4076中
  28:    e3a03456     mov    r3, #1442840576    ; 0x56000000
  2c:    e2833054     add    r3, r3, #84    ; r3=0x56000054
  30:    e50b3014     str    r3, [fp, #-20];fp=fp-20=4072,把0x56000054放入內存4072中
  34:    e51b2010     ldr    r2, [fp, #-16];r2=[fp-16=4076]=0x56000050
  38:    e3a03c01     mov    r3, #256; r3==256=0x100
  3c:    e5823000     str    r3, [r2];把0x100存入[0x56000050]
  40:    e51b2014     ldr    r2, [fp, #-20];r2=[fp-20=4072]=0x56000054
  44:    e3a03000     mov    r3, #0    ; r3=0=0x0
  48:    e5823000     str    r3, [r2];把0x0存入0x56000054內存中
  4c:    e3a03000     mov    r3, #0    ; r3=0x0
  50:    e1a00003     mov    r0, r3;r0=r3=0x0,這裏編譯器有點笨,可以直接r0給0的,這裏對應return 0.
  54:    e24bd00c     sub    sp, fp, #12    ; sp=fp-12=4080
  58:    e89da800     ldmia    sp, {fp, sp, pc}
  ;恢復保存的現場,ldmia,事後增加,從sp所對應內存塊中取出數據存放到後面的寄存器,高編號的寄存器放在高地址,低編號的寄存器
  ;放在低地址,此時sp=4080,從低地址往高地址開始恢復,這也符合ia後綴
  ;先恢復fp,此時4080地址存放的值,註意是值不是地址,等於4092,恰好就是等於fp,即一頓操作之後,fp還是等於原來的fp
  ;再恢復sp,sp=sp+4=4084,內存4084對應的值是4096,即一頓操作之後,sp又等於4096了;
  ;最後恢復pc,sp=sp+4=4088,內存4088對應的值是8,即一段操作之後,pc=8了,pc等於8意味著什麽?意味著函數從main函數返回了,將去執行那個死循環halt

為了更好的理解函數入棧,讓我們深入理解C語言底層匯編,畫出內存示意圖:

技術分享

你可能會說。0-4096不是4097了嗎?這樣問非常好,不放過任何有疑問的細節,但是,4096,可以是4096的開始,也可以是4095的結尾,這裏表示的是4095的結尾,因為4096我們一來是要先減4的。上圖的4096是沒用使用到真正屬於4096地址後擴4字節的,而是在剛好到達4096時,之前的內存。

到這裏,終於完成了一大半,我們知道了函數入棧之後,似乎函數調用的參數傳遞,也要用到棧啊,那麽我們繼續挖掘匯編。這裏又需要補充幾點arm方面的知識。

技術分享

技術分享

技術分享

技術分享

現在編寫匯編代碼, 傳遞一個參數:

.text
.global _start

_start:

    /* 設置內存: sp 棧 */
    ldr sp, =4096  /* nand啟動 */
//    ldr sp, =0x40000000+4096  /* nor啟動 */

    mov r0, #4
    bl led_on

    ldr r0, =100000
    bl delay

    mov r0, #5
    bl led_on

halt:
    b halt

對應的c代碼:

void delay(volatile int d)
{
    while (d--);
}

int led_on(int which)
{
    unsigned int *pGPFCON = (unsigned int *)0x56000050;
    unsigned int *pGPFDAT = (unsigned int *)0x56000054;

    if (which == 4)
    {
        /* 配置GPF4為輸出引腳 */
        *pGPFCON = 0x100;
    }
    else if (which == 5)
    {
        /* 配置GPF5為輸出引腳 */
        *pGPFCON = 0x400;
    }
    
    /* 設置GPF4/5輸出0 */
    *pGPFDAT = 0;

    return 0;
}

上面匯編中,只對應一個參數,所以只用r0就可以達到函數參數傳遞的效果,匯編比較簡單就不贅述了。

這個例子是為了讓我們了解匯編通過寄存器傳遞函數參數,前提是必須先設置sp寄存器。

甚至不必編寫main函數,當然不建議這樣做,這樣示例是為讓你明白一點,main函數也是因為啟動代碼去調用了它,我們通過改寫啟動代碼,可以沒有main函數。

終於到了我們最熟悉的階段了,編寫C語言應用程序,一般來說,復雜的和可復用性更高的代碼我們肯定是用C語言編寫,全用匯編編寫代碼真是很慢而且麻煩,更不用說機器碼編程了,但是了解它們對我們深入學習又十分有用,這或許就是arm對初學者不友好的原因吧,因為現在為止我們還沒有大型項目需要編寫復雜的Makefile,後面還有很多技能需要get,這也是在買了板子大半年了才真的開始上手的原因,需要花時間補習其他知識。

言歸正傳,如上面的C語言程序,雖然完成了要求,可是復用性太差,既然你覺得你C語言最熟悉,那麽就請封裝一個復用性高的代碼出來看看吧。

/*明天補c代碼,先洗漱休息了*/

C語言版——點亮LED燈,深入到棧