編譯過程並不神奇

編譯過程並不神奇

工具

過程的簡單描述

這篇文章儘量地說清楚從編譯程序到把代碼燒到板子裏,而後開始上電後運行程序。不少細節是根據Arduino來寫的,由於Arduino沒有外接的serial flash或者外接的SDRAM,因此相對來講簡單一些。但整體來講這個過程對於嵌入式設備都是十分類似的。php

當按下編譯按鈕後,編譯器compiler將每一個.c文件編譯成彙編語言,彙編語言是機器能夠識別的語言。拿項目舉例來講,LED.c通過編譯器生成LED.o。而後鏈接器linker把每一個彙編文件(*.o)鏈接在一塊兒,生成最終的編譯文件並經過avrdude或者programmer下載到arduino的Flash中開始運行。html

編譯過程

c 語言翻譯成彙編語言

  • Atmel 官方的彙編指令, 對於每一個嵌入式板子來講,每塊cpu都有它們本身不一樣的彙編語言指令。
  • 舉例來講,下面的c語言翻譯成avr
//c language
static void Task4(void)
{
  USART_Transmit("Task4\r\n", strlen("Task4\r\n"));
}

翻譯成彙編語言是git

ldi    r22, 0x07    ; 7    //"Task4\r\n" 字符串長度7的low byte 存入寄存器22
ldi    r23, 0x00    ; 0    //"Task4\r\n" 字符串長度7的high byte 存入寄存器23
ldi    r24, 0x1E    ; 30   //"Task4\r\n" 的內存位置0x011E的low byte存入寄存器24,這裏強調一下由於計算機架構的關係,這個位置在map文件中被翻譯成0080011E,後面會在詳細說明。
ldi    r25, 0x01    ; 1    //"Task4\r\n" 的內存位置0x011E的high byte存入寄存器24 
call    0x1f4    ; 0x1f4 <USART_Transmit>  //把當前的地址壓到stack裏,而後去地址0x1f4調用函數USART_Transmit,這個函數會利用r22,r23,r24和r25
ret                        //從stack裏面彈出返回地址,去這個返回地址開始運行接下來的命令

你們能夠參考Atmel官方對ret的解釋,能夠清楚的看到,ret命令把stack的東西彈出而後放入PC(Program Counter:裏面記錄着當前運行的代碼的位置)裏面。github

內存空間分紅幾個section

通過編譯器的編譯後,c文件中的代碼,和變量等會被存放到不一樣的區域section,參考GCC Sections
這裏面比較重要的是text section, data section和bss section:web

  • text section存放代碼
  • data section存放初始化過的變量和常量。初始化的時候,會從Flash裏把初始化值拷貝到Ram裏。請參考後文,有詳細的初始化彙編語言。
  • bss section存放未通過初始化的變量。初始化的時候,整個區域都會被初始化爲0。請參考後文,有詳細的初始化彙編語言。

舉個例子來講明,對於下面的c代碼:架構

uint16_t Data = 0x1234;
uint32_t Result;

uint32_t Power(void)
{
  Result = Data * Data;
  USART_Transmit("done!", 5);
  return Result;
}

通過編譯後咱們能夠獲得:
圖片描述函數

  • symbol table裏面記錄了text 和data section的起始位置,以及每一個函數和變量相對於對應section的偏移。工具

    • 裏面的函數USART_Transmit 由於並無在這個文件中定義,因此編譯器並不知道它在哪裏,後面鏈接器linker會到其餘文件中找到這個函數的定義而後把它補上。
  • text section存着函數Power()的彙編語言,裏面會去地址0x00F0取得Data的值,還會把運算的結果保存到地址0x00F8的Result裏面。
  • data section存着Data的數值0x1234,還有ASCII字符串"done!"。在c語言裏字符串都是用0x0結尾的,0x0也會佔用一個byte的空間,這裏字符串的名字(String_1)是編譯器本身命名的,也多是其餘的名字。
  • bss section存着Result的數值,bss區域的數據都會初始化0。

Atmel328P的Flash和Ram

Arduino所使用的MCU是Atmel328P,根據數據手冊,Atmel328P有2KB的SRAM和32KB的Flash,以及1KB的EEPROM。細節請參考Atmel328P的section 12.2,我截取了數據手冊中的一幅圖:
圖片描述網站

  • 好比咱們在Arduino常用的一些GPIO的寄存器,PORTB, DDRB都是存在IO registers中的。咱們能夠找到詳細的register summary在數據手冊section 35。
  • 根據以前對編譯器section的講解,最後text section和data section都會下載到Arduino的Flash中,由於text section中的代碼在運行的時候並不會被改變,data section中的初始值在板子初始化的時候會從Flash中拷貝到SRAM中,bss section會在板子初始化的時候被初始化爲0。
  • 細看2KB的Internal SRAM:ui

    • 首先放的是Data section而後是BSS section,剩下的部分都是給Heap(堆)和Stack(棧)。簡單地說當咱們使用malloc拿到的內存都是從heap裏面取得的,而函數的參數,返回值以及在函數內聲明的local variable都是向棧裏面push進去和pop出來的。
    • 在嵌入式開發中,由於內存資源有限,常常地malloc/free堆的內存空間,會減低內存的利用率,因此通常狀況咱們不常用malloc去拿堆裏面的空間。

圖片描述

鏈接過程

當編譯器把全部的.c文件都編譯好後,鏈接器linker就會過來把全部的.c文件集合起來生成一個總的文件。在Atmel中,最後生成的是.elf文件。鏈接器的做用是把全部的undefined variable都找到,把它們的地址都補全,而後把所用的相對位置都計算出絕對位置,好比前面例子中,symbol table裏的函數USART_Transmit是未知位置的,這是linker就會去其餘編譯文件中找這個函數的定義,並獲得地址。最後生成的總的文件相似於第一張圖的樣子,也是開始是symbol table而後是text section,data section和bss section。

Atmel328P的編譯文件的鏈接

  • 鏈接器linker須要它的配置文件,被稱爲linker file。對於Atmel328P來講,這個文件叫avr51.x,它在Atmel Studio的安裝文件夾裏,個人路徑是(C:\Program Files (x86)\Atmel\Studio\7.0\toolchain\avr8\avr8-gnu-toolchain\avr\lib\ldscripts\avr51.x)。打開avr51.x。

    • 首先是定義了每一個section的位置,裏面的data ORIGIN前兩個數字0x80,eeprom ORIGIN前兩個數字0x81是和總線相關,並不意味着SRAM的地址真的從0x800100開始。
    • 而後也定義了在main函數以前,Flash中應該放哪些代碼,包括中斷向量,初始化stack pointer,heap的地址,初始化data section和bss section等等。
MEMORY
{
  text   (rx)   : ORIGIN = 0, LENGTH = __TEXT_REGION_LENGTH__
  data   (rw!x) : ORIGIN = 0x800100, LENGTH = __DATA_REGION_LENGTH__
  eeprom (rw!x) : ORIGIN = 0x810000, LENGTH = __EEPROM_REGION_LENGTH__
  fuse      (rw!x) : ORIGIN = 0x820000, LENGTH = __FUSE_REGION_LENGTH__
  lock      (rw!x) : ORIGIN = 0x830000, LENGTH = __LOCK_REGION_LENGTH__
  signature (rw!x) : ORIGIN = 0x840000, LENGTH = __SIGNATURE_REGION_LENGTH__
  user_signatures (rw!x) : ORIGIN = 0x850000, LENGTH = __USER_SIGNATURE_REGION_LENGTH__  
}
.text:
{
  *(.vectors)
    KEEP(*(.vectors))
    *(.init0)  /* Start here after reset.  */
    KEEP (*(.init0))
    *(.init1)
    KEEP (*(.init1))
    *(.init2)  /* Clear __zero_reg__, set up stack pointer.  */
    KEEP (*(.init2))
    *(.init3)
    KEEP (*(.init3))
    *(.init4)  /* Initialize data and BSS.  */
    KEEP (*(.init4))
    *(.init5)
    KEEP (*(.init5))
    ...
}

Atmel328P的Map和Lss文件

當Atmel328P編譯完成後,除了會生成最後的編譯文件.elf外,還會生成.map和.lss文件,不少時候這兩個文件能夠幫咱們很好的debug程序,理解程序。

  • .lss文件包括了整個項目的彙編代碼,text section的每條代碼。
  • .map就是一個symbol table,裏面包含了全部symbol的位置。

下面咱們來簡單分析一下兩個文件:

IoT_Ethernet.lss

在IoT_Ethernet.lss,我挑了幾段我認爲比較有意思的地方和你們分享下:

1. data, text, bss section的地址:

Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         00000064  00800100  00000676  0000070a  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  1 .text         00000676  00000000  00000000  00000094  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .bss          00000096  00800164  00800164  0000076e  2**0
                  ALLOC
  • VMA vs LMA:上表中記錄了每一個section的大小和地址,值得注意的是這裏有兩個地址,一個是VMA(Virtual Memory Address),另一個是LMA(Load Memory Address)。根據這個解釋,VMA是當板子經歷過初始化階段(startup)後,該section的地址;LMA是這個section的數據從哪裏加載。因此:

    • data section的大小是100bytes (十六進制0x64,十進制100),從地址0x800100開始,當初始化階段,會從地址0x676把data section拷貝到地址0x800100(這個是RAM地址)。這裏0x676是data section初始化數據在Flash中的地址,也正好是從text section結束的地方。
    • text section的大小是676bytes,從地址0x0開始,它的Load Memory Address 也是0x0。意味着不須要在初始化時候進行拷貝。
    • bss section的大小是96bytes,從地址0x800164開始,它的Load Memory Address 也是0x0。

2. 初始化data 和bss section的彙編代碼:

這段程序是在初始化data section。在這段asssembly code中,程序從Flash的地址0x676中複製0x64 bytes(十六進制0x64,十進制100)的數據到地址0x00800100。這裏注意兩條彙編,一個是LPM,一個是ST,從Atmel Assembly的官方網站,LPM: Load Program Memory,是從program memory拿出數據,這裏的program memory指的是Flash。另一個是ST: Store Indirect From Register to Data space using Index X,這裏的data section指的是SDRAM,SDRAM從0x00800000開始,因此仔細分析這些彙編會發現,這裏的st X+, r0,X寄存器(r26和r27合在一塊兒)的範圍是從0x100開始,但實際是把數據存到了0x00800100。

00000074 <__do_copy_data>:
  74:    11 e0           ldi    r17, 0x01    ; 1
  76:    a0 e0           ldi    r26, 0x00    ; 0
  78:    b1 e0           ldi    r27, 0x01    ; 1
  7a:    e6 e7           ldi    r30, 0x76    ; 118
  7c:    f6 e0           ldi    r31, 0x06    ; 6
  7e:    02 c0           rjmp    .+4          ; 0x84 <__do_copy_data+0x10>
  80:    05 90           lpm    r0, Z+
  82:    0d 92           st    X+, r0
  84:    a4 36           cpi    r26, 0x64    ; 100
  86:    b1 07           cpc    r27, r17
  88:    d9 f7           brne    .-10         ; 0x80 <__do_copy_data+0xc>

3. 中斷向量:

當中斷髮生時,程序會跟據具體是哪一個中斷向量(好比定時器中斷,外部中斷等)來這個中斷向量表中找到中斷ISR(Interrupt Service Routine)的地址。好比我目前的這個中斷向量表中,當Timer0的compare match interrupt發生的時候,程序會到0x59C執行ISR;當UART接收到數據的時候,程序會到0x222執行ISR。以下面的代碼是中斷向量表的一部分:

00000000 <__vectors>:
   0:    0c 94 34 00     jmp    0x68    ; 0x68 <__ctors_end>
   4:    0c 94 51 00     jmp    0xa2    ; 0xa2 <__bad_interrupt>
   8:    0c 94 51 00     jmp    0xa2    ; 0xa2 <__bad_interrupt>
...
  38:    0c 94 ce 02     jmp    0x59c    ; 0x59c <__vector_14>
...
  48:    0c 94 11 01     jmp    0x222    ; 0x222 <__vector_18>
...

IoT_Ethernet.map文件

這個文件能夠理解爲是一個symbol table,裏面包括了項目全部symbol的信息。好比

.text.LED_GetStatus
                0x00000428       0x20 LowLevel/LED.o
                0x00000428                LED_GetStatus
  • 函數LED_GetStatus是在Flash地址0x428,assembly code一共佔32 Byte。
  • 一樣在data和bss section也包含了不少信息。

Motorola Hex Format(.hex)

由於咱們使用AvrDude經過USB下載代碼到板子上,而AvrDude只接收Intel Hex Format,生成能被Arduino Bootloader識別的數據。使用WinAVR能夠將.elf文件傳變HEX文件(Atmel Studio已經幫咱們作這一步了)。簡單地說Intel Hex Format每行在說往某個特定的地址寫特定的數據。Intel Hex Format參考連接
提取出.hex文件中比較直觀的一行來講:

:10066800FFFF4765744C6564537461747573005378
  • 這裏是說把0xFF, 0xFF, "GetLedStatus", 0x53, 0x78拷貝到地址0x668。
  • AvrDude根據hex文件生成Arduino bootloader能夠識別的數據經過USB接口發送給Arduino。後面bootloader的部分會講發送的數據。

Arduino Bootloader

Bootloader是在Flash裏面的一段代碼,用來把新的代碼(新的代碼從AvrDude發到Arduino的16U2芯片,而後16U2芯片經過uart發送到Atmel328P)經過USB寫到Flash的0x00地址。若是沒有BootLoader,咱們只能經過programmer來燒代碼到Arduino,下圖是Avr ISP MKII,把它插到Arduino的ICSP header上,就能夠在沒有bootloader的狀況下給Arduino下載代碼了。若是是新購買的Arduino,它裏面已經有Bootloader了,它使用的是optiboot,它是open source的,這裏是它的github repository。下面咱們更詳細的說下bootloader。
圖片描述

FUSE 設置

在以前的linker file裏面有Fuse的地址:fuse (rw!x) : ORIGIN = 0x820000, LENGTH = __FUSE_REGION_LENGTH__。Fuse裏面的信息是配置芯片的關鍵信息,查看和修改它的信息須要用ISP,通常USB接口沒辦法訪問Fuse。

  • Atmel328P data sheet的第30章,Boot Loader Support詳細說明了Fuse的信息。這裏我附裏面的兩張圖:能夠看到,在Flash的底端是bootloader區域,咱們正常的代碼每次是下載到從0x0000開始的區域。配置Fuse寄存器,咱們能夠告訴芯片目前芯片裏是否存了bootloader而後bootloader的大小是多少,bootloader的起始地址是什麼。當芯片開始上電的時候,芯片其實是從bootloader的起始位置開始運行代碼,它會在bootloader裏面停很短的時間,看是否有新的代碼要下載到0x0000地址,若是沒有就去0x0000開始執行那裏的代碼,若是有就下載新的代碼到0x0000地址。
    圖片描述
    圖片描述

Optiboot

當AvrDude拿到Intel Hex Format(.hex)的代碼,它會轉變成Atmel STK500格式的代碼,由於optiboot能夠識別Atmel STK500 格式。當AvrDude要發送新的代碼給Atmel328P,這是Atmel328P會先reset,而後AvrDude會發送STK500格式的數據給optiboot,optiboot會處理這些數據而後把相應的代碼從Flash的0x0000地址開始寫入。 當代碼下載完成,就從新reset芯片,而且等待程序跳出optiboot,就能夠從0x0000開始執行新的代碼了!

相關文章
相關標籤/搜索