使用GNU工具鏈進行嵌入式裸機開發


Embedded-Programming-with-the-GNU-Toolchain

Vijay Kumar B.

vijaykumar@bravegnu.org

翻譯整理:thammer github:https://github.com/tanghammer/Embedded-Programming-with-the-GNU-Toolchain.git

目錄

1.介紹

2.建立ARM實驗室

3.Hello ARM

4.更多的匯編器指令

5.使用RAM

6.鏈接器

7.鏈接腳本文件

8.數據位於RAM中示例

9.異常處理

10.C啟動代碼

11.使用C庫

12.內聯匯編

13.貢獻

14.Credits

附錄

1.介紹

GNU工具鏈越來越多地用於深度嵌入式軟件開發。這種類型的軟件開發也稱為獨立C語言編程和裸機C語言編程。獨立的C語言編程帶來了新的問題,處理這些問題需要對GNU工具鏈有更深入的理解。GNU工具鏈的手冊提供了關於工具鏈的優秀信息,但是是從工具鏈的角度,而不是從問題的角度。不管怎樣,手冊就是這樣寫的。其結果是對常見問題的答案分散在各地,GNU工具鏈的新用戶感到困惑。

本教程試圖通過從問題的角度解釋這些工具來彌補這一差距。希望這能使更多的人在他們的嵌入式項目中使用GNU工具鏈。

本教程使用Qemu模擬了一個基於ARM的嵌入式系統。有了它,您可以從舒適的桌面環境中學習GNU工具鏈,而無需在硬件上進行投資。本教程本身並不教授ARM指令集。它應該與其他書籍和在線教程一起使用,比如:

但是為了方便讀者,附錄中列出了常用的ARM指令。

2.建立ARM實驗室

本節展示如何使用Qemu和GNU工具鏈在您的PC中設置一個簡單的ARM開發和測試環境。Qemu是一個機器模擬器,能夠模擬各種機器,包括基於ARM的機器。您可以編寫ARM匯編程序,使用GNU工具鏈編譯它們,並在Qemu中執行和測試它們。

2.1.Qemu ARM

Qemu將用於模擬Gumstix基於PXA255的connex板。您應該至少擁有0.9.1版本的Qemu來使用本教程。

PXA255有一個ARMv5TE指令集的ARM內核。PXA255也有幾個片上外設。本教程將介紹一些外圍設備。

2.2.在Debian中安裝Qemu

本教程要求qemu版本0.9.1或更高。Debian Squeeze/Wheezy中提供的qemu包滿足這一要求。使用apt-get安裝qemu

$ apt-get install qemu

2.3.安裝ARM GNU工具鏈

  1. CodeSourcery (Mentor Graphics的一部分)提供了可用於各種體系架構的GNU工具鏈。下載用於ARM的GNU工具鏈,可從: http://www.mentor.com/embedded-software/sourcery-tools/sourcery-codebench/editions/lite-edition/

  2. 解壓至~/toolchains

     $ mkdir ~/toolchains
     $ cd ~/toolchains
     $ tar -jxf ~/downloads/arm-2008q1-126-arm-none-eabi-i686-pc-linux-gnu.tar.bz2
    
  3. 工具鏈路徑添加至環境變量PATH

     $ PATH=$HOME/toolchains/arm-2008q1/bin:$PATH
    
  4. .bashrc中添加並導出PATH

3.Hello ARM

在本節中,您將學習如何匯編一個簡單的ARM程序,並用Qemu模擬connex裸機板進行測試。

匯編程序源文件由一系列語句組成,每行一個。每個語句都具有以下格式。

label:    instruction         @ comment

每個部分都是可選的。

label:

  • 標簽是一種方便的方法來引用指令在內存中的位置。標簽可以用於任何地址出現的地方,例如作為分支指令的操作數(b label)。標簽由字母,數字,_和$組成。

comment:

  • 注釋以@開頭,在@號之后出現的字符會被編譯器忽略。

instruction:

  • 指令可以是ARM指令集里面的指令或者匯編器的指令。匯編器的指令是給匯編器的命令。匯編器指令由.號打頭。

下面是一個非常簡單的ARM匯編程序,實現2個數相加。

Listing 1. Adding Two Numbers
        .text
start:                       @ Label, not really required
        mov   r0, #5         @ Load register r0 with the value 5
        mov   r1, #4         @ Load register r1 with the value 4
        add   r2, r1, r0     @ Add r0 and r1 and store in r2

stop:   b stop               @ Infinite loop to stop execution

.text是一個匯編器指令,是說接下來的指令必須匯編到代碼段(code section),而不是數據段(data section)。段(sections)這個概念會在后面的教程中詳細介紹。

3.1.構建二進制文件

將程序保存至文件add.s中。要匯編此文件,需要調用GNU工具鏈的匯編器as,命令如下。

$ arm-none-eabi-as -o add.o add.s

-o選項指定了輸出文件的名字。

注意:交叉工具鏈總是以構建它們的目標體系結構為前綴,以避免與主機工具鏈的名稱沖突。為了可讀性,我們將在文本中引用不帶前綴的工具。

要生成可執行文件,需要調用GNU工具鏈的連接器ld,命令如下。

$ arm-none-eabi-ld -Ttext=0x0 -o add.elf add.o

這里,-o選項再次指定了輸出文件名。-Ttext=0x0,指定了分配給標簽的地址,以便指令從地址0x0開始。可以通過nm命令查看各標簽的地址分配情況。

$ arm-none-eabi-nm add.elf 
00008010 T __bss_end__
00008010 T _bss_end__
00008010 T __bss_start
00008010 T __bss_start__
00008010 T _edata
00008010 T _end
00008010 T __end__
00000000 t start
         U _start
0000000c t stop

現在只關注標簽startstop。注意標簽startstop的地址分配。分配給start的地址是0x0。因為它是第一條指令的標簽。標簽stop在3條指令之后。每條指令占4個字節。因此stop分配的地址是12(0xC)

為指令鏈接不同的基址將導致為標簽分配一組不同的地址。

$ arm-none-linux-gnueabi-ld -Ttext=0x20000000 add.o -o add.elf 
$ arm-none-linux-gnueabi-nm add.elf
20008010 T __bss_end__
20008010 T _bss_end__
20008010 T __bss_start
20008010 T __bss_start__
20008010 T _edata
20008010 T _end
20008010 T __end__
20000000 t start
         U _start
2000000c t stop

ld生成的輸出文件格式是ELF。有多種文件格式可用於存儲可執行代碼。ELF格式在有操作系統的情況下工作得很好,但是由於我們要在裸機上運行程序,所以必須將其轉換為更簡單的文件格式,稱為二進制格式

二進制格式的文件包含特定內存地址的連續字節。該文件中不存儲其他附加信息。這對於Flash編程工具來說很方便,因為編程時所要做的就是將文件中的每個字節復制到從內存中指定的基址開始的連續地址。

GNU工具鏈的objcopy命令能用於轉換不同的目標文件格式。常見的用法如下。

objcopy -O <output-format> <in-file> <out-file>

下面的命令用於將add.elf轉換為二進制格式。

$ arm-none-eabi-objcopy -O binary add.elf add.bin

檢查文件的大小。該文件將恰好是16個字節。因為有4條指令,每條指令占用4個字節。

$ ls -la add.bin 
-rwxrwxr-x 1 thomas thomas 16 3月  23 17:56 add.bin

3.2.在Qemu里面執行

當ARM處理器復位時,它就從0x0地址開始執行。在connex板上有一個16MB的Flash位於地址0x0。在Flash開始處的指令將被執行。

當用qemu仿真connex板時,必須指定將哪個文件當作Flash使用。Flash文件給誰非常簡單。為了從Flash中的地址X獲取字節,qemu從文件中的偏移量X讀取字節。事實上,這與二進制文件格式相同。

為了測試這個程序,我們首先在模擬的Gumstix connex板上創建一個16MB的文件來表示Flash。我們使用dd命令將16MB的0從/dev/zero復制到文件flash.bin。數據以4K塊的形式復制。

$ dd if=/dev/zero of=flash.bin bs=4096 count=4096

然后使用以下命令將add.bin文件復制到Flash的開頭。

$ dd if=add.bin of=flash.bin bs=4096 conv=notrunc

這相當於將bin文件燒錄到Flash上。

復位之后,處理器將從地址0x0開始執行,程序中的指令將被執行。下面給出了調用qemu的命令。

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null

-M connex選項指定要模擬的機器是connex-pflash選項指定Flash .bin文件表示閃存。-nographic指定不需要模擬圖形顯示。-serial /dev/null指定將connex板的串口連接到/dev/null,以便丟棄串口數據。

系統執行指令,完成后,在stop: b stop指令中無限循環。要查看寄存器的內容,可以使用qemu的監視器接口。監控接口是一個命令行接口,通過它可以控制仿真系統並查看系統狀態。當qemu使用上述命令啟動時,監視器接口在qemu的標准I/O中提供(就是指可以以敲命令的方式交互)。

要查看寄存器的內容,可以使用info register命令。

(qemu) info registers
R00=00000005 R01=00000004 R02=00000009 R03=00000000
R04=00000000 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=00000000 R14=00000000 R15=0000000c
PSR=400001d3 -Z-- A svc32

注意寄存器R02中的值。寄存器包含加法的結果,並且應該與期望值9匹配。

3.3.更多的監視器命令

表列出了一些有用的qemu監視器命令。

命令 用途
help 列出可用的命令
quit 退出模擬器
xp /fmt addr 從addr轉儲物理內存
system_reset 復位系統

詳細解釋下xp命令。fmt參數指定如何顯示內存內容。fmt的語法是<count><format><size>

count

  • 指定count個要轉儲的數據項。

size

  • 指定每個數據項的大小。b代表8位,h代表16位,w代表32位,g代表64位。

format

  • 指定顯示格式。x表示十六進制,d表示有符號十進制數,u表示無符號十進制數,o表示八進制,c表示char, i表示asm指令。

使用帶有i格式的xp命令,可以用來反匯編內存中的指令。要反匯編位於0x0的指令,可以使用xp命令,將fmt指定為4iw4指定要顯示4項,i指定要打印的項作為指令(相當於一個內置的反匯編器!),w指定這些項的大小為32位。命令的輸出如下所示。

(qemu) xp /4iw 0x0
0x00000000:  mov        r0, #5  ; 0x5
0x00000004:  mov        r1, #4  ; 0x4
0x00000008:  add        r2, r1, r0
0x0000000c:  b  0xc

4.更多的匯編器指令

在此章節,我們通過2個示例程序介紹常用的匯編器指令。

  1. 對數組求和的程序
  2. 計算字符串長度的程序

4.1.數組求和

下面的代碼對一個數組求和,並將結果存儲在r3中。

Listing 2. Sum an Array
        .text
entry:  b start                 @ Skip over the data
arr:    .byte 10, 20, 25        @ Read-only array of bytes
eoa:                            @ Address of end of array + 1

        .align
start:
        ldr   r0, =eoa          @ r0 = &eoa
        ldr   r1, =arr          @ r1 = &arr
        mov   r3, #0            @ r3 = 0
loop:   ldrb  r2, [r1], #1      @ r2 = *r1++
        add   r3, r2, r3        @ r3 += r2
        cmp   r1, r0            @ if (r1 != r2)
        bne   loop              @    goto loop
stop:   b stop

該代碼引入了兩個新的匯編器指令--.byte.align。下面將描述這些匯編器指令。

4.1.1. .byte指令

.byte的字節大小參數被匯編成內存中的連續字節。對於存儲16位值和32位值,有類似的.2byte.4byte指令。一般語法如下所示。

.byte   exp1, exp2, ...
.2byte  exp1, exp2, ...
.4byte  exp1, exp2, ...

參數可以是簡單的整數,表示為二進制(前綴為0b0B)、八進制(前綴為0)、十進制(無前綴,不要以0開頭,避免被當做八進制處理)或十六進制(前綴為0x0X)。整數也可以表示為字符常量(由單引號包圍的字符),在這種情況下將使用字符的ASCII值。

參數也可以是由文字和其他符號構造的C表達式。示例如下所示。

pattern:  .byte 0b01010101, 0b00110011, 0b00001111
npattern: .byte npattern - pattern
halpha:   .byte 'A', 'B', 'C', 'D', 'E', 'F'
dummy:    .4byte 0xDEADBEEF
nalpha:   .byte 'Z' - 'A' + 1

4.1.2. .align指令

ARM要求指令出現在32位對齊的內存位置。指令中4個字節中的第一個字節的地址應該是4的倍數。要做到這一點,可以使用.align指令插入填充字節,直到下一個字節地址是4的倍數。只有當在代碼中插入數據字節或半字時才需要這樣做。

4.2.字符串長度

下面的代碼計算字符串的長度,並將長度存儲在寄存器r1中。

Listing 3. String Length
        .text
        b start

str:    .asciz "Hello World"

        .equ   nul, 0

        .align
start:  ldr   r0, =str          @ r0 = &str
        mov   r1, #0

loop:   ldrb  r2, [r0], #1      @ r2 = *(r0++)
        add   r1, r1, #1        @ r1 += 1
        cmp   r2, #nul          @ if (r1 != nul)
        bne   loop              @    goto loop

        sub   r1, r1, #1        @ r1 -= 1
stop:   b stop

該代碼引入了兩個新的匯編器指令- .asciz.equ。下面描述匯編器指令。

4.2.1. .asciz指令

.asciz指令接受字符串文本作為參數。字符串文字是雙引號中的字符序列。字符串文字被匯編成連續的內存位置。匯編器在每個字符串后面自動插入一個nul字符(\0字符)。

'ascii指令與.asciz相同,但是匯編器不會在每個字符串后面插入nul字符。

4.2.2. .equ指令

匯編器維護稱為符號表的東西。符號表將標簽名稱映射到地址。每當匯編器遇到標簽定義時,匯編器在符號表中做一個入口點。每當匯編器遇到標簽引用時,它就用符號表中對應地址替換標簽。

使用匯編器的指令.equ,還可以手動插入符號表中的條目,將名稱映射到不一定是地址的值。每當匯編器遇到這些名稱時,它就用它們對應的值替換它們。這些名稱和標簽名稱一起稱為符號名。

該指令的一般語法如下所示。

.equ name, expression

name是一個符號名稱,並且具有與標簽名稱相同的限制。expression可以是簡單的字符,也可以是.byte指令解釋的表達式。

注意: 與.byte指令不同,.equ指令本身不分配任何內存。它們只是在符號表中創建條目。

5.使用RAM

存儲前面示例程序的閃存是一種EEPROM。它是一個有用的輔助存儲,就像硬盤一樣,但是不方便在Flash中存儲變量。變量應該存儲在RAM中,這樣就可以很容易地修改它們。

connex板有一個64 MB的RAM,從地址0xA0000000開始,其中可以存儲變量。connex板的內存映射如下圖所示。

Figure 1. Memory Map

必須進行必要的設置才能將變量放在這個地址。要理解必須做什么設置,必須理解匯編器和鏈接器的角色。

6.鏈接器

當編寫一個多源文件的程序時,每個文件被單獨匯編為目標文件。鏈接器將這些目標文件組合起來形成最終的可執行文件。

Figure 2. Role of the Linker

當組合目標文件在一起時,鏈接器執行了如下操作。

  1. 符號解析
  2. 重定位

在本節中,我們將詳細研究這些操作。

6.1.符號解析

在單文件程序中,在生成目標文件時,所有對標簽的引用都由匯編器用它們的對應地址替換。但在多文件程序中,如果有對另一個文件中定義的標簽的任何引用,則匯編器將這些引用標記為“未解析(unresolved)”。當這些目標文件傳遞給鏈接器時,鏈接器將從其他目標文件確定這些引用的值,並使用正確的值對代碼進行調整(patch)。

sum of array示例被分成兩個文件,以演示鏈接器執行符號解析。這兩個文件將被匯編起來,並檢查它們的符號表,以顯示未解析引用的存在。

文件sum-sub.s包含sum子程序,文件main.s傳入所需的參數調用子程序。這些文件的源代碼如下所示。

Listing 4. main.s - Subroutine Invocation
        .text
        b start                 @ Skip over the data
arr:    .byte 10, 20, 25        @ Read-only array of bytes
eoa:                            @ Address of end of array + 1

        .align
start:
        ldr   r0, =arr          @ r0 = &arr
        ldr   r1, =eoa          @ r1 = &eoa

        bl    sum               @ Invoke the sum subroutine

stop:   b stop
Listing 5. sum-sub.s - Subroutine Definition
        @ Args
        @ r0: Start address of array
        @ r1: End address of array
        @
        @ Result
        @ r3: Sum of Array

        .global sum

sum:    mov   r3, #0            @ r3 = 0
loop:   ldrb  r2, [r0], #1      @ r2 = *r0++    ; Get array element
        add   r3, r2, r3        @ r3 += r2      ; Calculate sum
        cmp   r0, r1            @ if (r0 != r1) ; Check if hit end-of-array
        bne   loop              @    goto loop  ; Loop
        mov   pc, lr            @ pc = lr       ; Return when done

關於.global指令的解釋是由必要的。在C中,在函數外部聲明的所有變量對其他文件都是可見的,直到明確說明為static。在匯編中,所有標簽都是static的,也稱本地(對文件而言),直到明確聲明它們應該對其他文件可見,這時就需要使用.global指令來修飾。

文件被匯編后,並使用nm命令轉儲符號表。

$ arm-none-eabi-as -o main.o main.s
$ arm-none-eabi-as -o sum-sub.o sum-sub.s
$ arm-none-eabi-nm main.o
00000004 t arr
00000007 t eoa
00000008 t start
00000018 t stop
         U sum
$ arm-none-eabi-nm sum-sub.o
00000004 t loop
00000000 T sum

現在,關注第二列的字母,它指定了符號的類型。t表示這個符號在text段是定義了的。u表示這個符號未定義。大寫字母表示符號是.global的。

很明顯符號sumsum-sub.o中定義了,不過在main.o中還未解析。當鏈接器被調用后,符號引用被解析,然后生成可執行文件。

6.2.重定位

重定位是改變標簽已分配地址的過程。這還包括調整所有標簽的引用地址以使對應上最新分配的地址。重定位主要基於以下兩個原因:

  1. 段合並
  2. 段排布

要理解重新定位的過程,理解段的概念是必不可少的。

代碼和數據有不同的run-time需求。例如代碼可以放置在只讀的存儲介質中,數據則要放置在讀-寫存儲介質中。如果代碼和數據不混在一起,也許會更合適。為此,程序被分割為段。大多程序至少包含2個段,.text段放代碼,.data段放數據。匯編器指令.text.data用於在這兩個段間來回切換。

可以把每個段想象成一個桶。當匯編器識別到一個段指令時,它會把緊跟指令的代碼/數據放到對應的桶里面。因此,屬於特定段的代碼/數據的位置是緊挨着的。下面的圖顯示了匯編器如何將數據重新排列到段中。

Figure 3. Sections

現在我們已經了解了段,讓我們看看執行重定位的主要原因。

6.2.1.段合並

當處理多源文件程序時,在每個文件中可能會出現同樣名字的段(例如.text)。鏈接器負責將輸入文件的段合並到一起放到輸出文件對應的段中。默認情況下,擁有同樣名字的段會被放置到一起,標簽引用的地址也會被調整到對應的新的地址上。

通過查看目標文件和相應的可執行文件的符號表,可以看到段合並的效果。多源文件的sum of array程序可以用來闡明段合並。目標文件main.osum-sum.o和可執行文件sum.elf的符號表如下所示。

$ arm-none-eabi-nm main.o
00000004 t arr
00000007 t eoa
00000008 t start
00000018 t stop
         U sum
$ arm-none-eabi-nm sum-sub.o
00000004 t loop ❶
00000000 T sum
$ arm-none-eabi-ld -Ttext=0x0 -o sum.elf main.o sum-sub.o
$ arm-none-eabi-nm sum.elf
...
00000004 t arr
00000007 t eoa
00000008 t start
00000018 t stop
00000028 t loop ❷
00000024 T sum

❶ ❷ loop符號在sum-sub.o中地址是0x4,在sum.elf中地址是0x28。這是因為sum-sub.o.text段恰好放置在main.o.text之后。

6.2.2.段排布

當一個程序被匯編后,它的每個段都假定從0地址開始。因此,標簽被分配的值是相對於段的起始處的。最后可執行文件生成時,段被放置到某個地址X上。並且所有對該部分中定義的標簽的引用都被加上一個X的偏移,因此它們指向新的位置。

每個段在內存中的特定位置的排布,對段中每個標簽引用的調整都是由鏈接器來完成的。

通過查看目標文件的符號表和相應的可執行文件,可以看到段排布的效果。單源文件的sum of array程序可以用來說明節的位置。為了更清楚,我們將把.text段放在地址0x100處。

$ arm-none-eabi-as -o sum.o sum.s
$ arm-none-eabi-nm -n sum.o
00000000 t entry ❶
00000004 t arr
00000007 t eoa
00000008 t start
00000014 t loop
00000024 t stop
$ arm-none-eabi-ld -Ttext=0x100 -o sum.elf sum.o ❷
$ arm-none-eabi-nm -n sum.elf
00000100 t entry ❸
00000104 t arr
00000107 t eoa
00000108 t start
00000114 t loop
00000124 t stop
...

❶ 在一個段內,標簽的地址從0開始分配。

❷ 創建可執行文件時,指定鏈接器將.text段放在地址0x100處。

.text段中標簽的地址被從地址0x100處開始重新分配,所有標簽引用都被調整反應了這一點。

段合並、段排布的過程如下圖所示。

Figure 4. Section Merging and Placement

7.鏈接腳本文件

如前一節所述,段的合並和段的排布是由鏈接器完成的。編程人員可以通過一個鏈接腳本文件控制段如何合並以及它們在內存中的位置。下面是一個非常簡單的鏈接腳本。

Listing 6. Basic linker script
SECTIONS { ❶
        . = 0x00000000; ❷
        .text : { ❸
                abc.o (.text);
                def.o (.text);
        } ❹
}

SECTIONS命令是最重要的鏈接器命令,它指定了如何合並這些段以及將它們放置在什么位置。

❷ 在SECTIONS命令之后的語句塊,.(dot)表示位置計數器。位置總是初始化為0x00000000。可以通過給它賦一個新的值來修改它。將開始處的位置計數器賦值為0x00000000是多余的。

❸ ❹ 鏈接腳本的這個部分指定了,輸入文件abc.odef.o.text段將被放置到輸出文件的.text段。

通過使用通配符*而不是單獨指定文件名,可以進一步簡化和通用化鏈接器腳本。

Listing 7. Wildcard in linker scripts
SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }
}

如何程序既包含.text段也包含.data段,.data段的合並和位置可以像下面這樣指定。

Listing 8. Multiple sections in linker scripts
SECTIONS {
         . = 0x00000000;
         .text : { * (.text); }

         . = 0x00000400;
         .data : { * (.data); }
}

此處.text段被放置到地址0x00000000處,.data被放置到地址0x00000400處。注意,如果位置計數器未分配不同的值,則.text.data段會被放置到相鄰的存儲位置。

7.1.鏈接腳本示例

為了演示鏈接器腳本的使用,我們將使用[listing-8-multiple-sections-in-linker-scripts](Listing 8. Multiple sections in linker scripts)中所示的鏈接器腳本來控制程序的.text.data段的排布。為此,我們將使用稍微修改過的sum of array程序。代碼如下所示。

        .data
arr:    .byte 10, 20, 25        @ Read-only array of bytes
eoa:                            @ Address of end of array + 1

        .text
start:
        ldr   r0, =eoa          @ r0 = &eoa
        ldr   r1, =arr          @ r1 = &arr
        mov   r3, #0            @ r3 = 0
loop:   ldrb  r2, [r1], #1      @ r2 = *r1++
        add   r3, r2, r3        @ r3 += r2
        cmp   r1, r0            @ if (r1 != r2)
        bne   loop              @    goto loop
stop:   b stop

這里唯一的變化是數組現在位於.data部分。還要注意,不需要使用額外的分支指令跳過數據部分,因為鏈接器腳本將適當地放置.text段和.data段。因此,語句可以以任何方便的方式放置在程序中,而鏈接腳本將負責將這些段正確地放置在內存中。

當一個程序被鏈接時,鏈接腳本作為一個輸入傳給鏈接器,如下命令。

$ arm-none-eabi-as -o sum-data.o sum-data.s
$ arm-none-eabi-ld -T sum-data.lds -o sum-data.elf sum-data.o

選項-T sum-data.lds指定了sum-data.lds將作為鏈接腳本。轉儲符號表,將使您了解如何在內存中放置段。

$ arm-none-eabi-nm -n sum-data.elf
00000000 t start
0000000c t loop
0000001c t stop
00000400 d arr
00000403 d eoa

從符號表中可以明顯看出·text段是從地址0x0開始放置的,.data段是從地址0x400開始放置的。

8.數據位於RAM中示例

現在我們知道了如何編寫鏈接器腳本,我們將嘗試編寫一個程序,並將.data部分放在RAM中。

add程序修改為從RAM加載兩個值,將它們相加並將結果存儲回RAM。兩個值和結果存放在.data段。

Listing 9. Add Data in RAM
        .data
val1:   .4byte 10               @ First number
val2:   .4byte 30               @ Second number
result: .4byte 0                @ 4 byte space for result

        .text
        .align
start:
        ldr   r0, =val1         @ r0 = &val1
        ldr   r1, =val2         @ r1 = &val2

        ldr   r2, [r0]          @ r2 = *r0
        ldr   r3, [r1]          @ r3 = *r1

        add   r4, r2, r3        @ r4 = r2 + r3

        ldr   r0, =result       @ r0 = &result
        str   r4, [r0]          @ *r0 = r4

stop:   b stop

使用下面的鏈接腳本。

SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }

        . = 0xA0000000;
        .data : { * (.data); }
}

elf文件的符號表轉儲如下所示。

$ arm-none-eabi-nm -n add-mem.elf
00000000 t start
0000001c t stop
a0000000 d val1
a0000001 d val2
a0000002 d result

鏈接腳本似乎已經解決了在RAM中放置.data段的問題。但是等等,解決方案還沒有完成!

8.1.RAM是易失性的

RAM是易失性的存儲介質,因此不可能在開機時直接在RAM中使數據。(要從非易失性的存儲介質復制過來)

所有代碼和數據都應該在開機前存儲在Flash中。在啟動時,啟動代碼應該將數據從Flash復制到RAM,然后繼續執行程序。因此,程序的.data段有兩個地址,Flash中的加載地址和RAM中的運行時地址。

Tip

ld的說法中,加載地址稱為LMA(Load Memory Address),run-time地址稱為VMA(Virtual Memory Address)。

為了讓程序正常運行,需要做下面2個修改。

  1. 鏈接器需要同時指定.data段的加載地址和運行地址。
  2. 一段用於將.data段從Flash(加載地址)拷貝至RAM(運行地址)的代碼。

8.2.指定加載地址

run-time地址應該用於確定標簽的地址。在前面的鏈接腳本中,我們為.data段指定了運行地址,但是加載地址沒有顯式指定,而是默認為運行時地址。對於前面的示例,這是可以的,因為程序是直接從Flash執行的。但是,如果要在執行期間將數據放在RAM中,那么加載地址應該與Flash對應,而運行時地址應該與RAM對應。

可以使用AT關鍵字指定與運行地址不同的加載地址。修改后的鏈接器腳本如下所示。

SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }
        etext = .; ❶

        . = 0xA0000000;
        .data : AT (etext) { * (.data); } ❷
}

❶ 可以在SECTIONS命令中通過為符號賦值來動態創建符號。這里,etext被賦值為該位置的位置計數器的值。etext包含代碼段之后Flash中的下一個空閑位置的地址。稍后將使用它來指定.data段在Flash中的位置。注意etext本身不會分配任何內存,它只是符號表中的一個條目。

❷ AT關鍵字指定.data段的加載地址。地址或符號(其值是有效地址)可以作為參數傳遞給AT。這里.data的加載地址被指定為Flash中代碼段之后的位置。

8.3.拷貝.data至RAM

要拷貝.data至RAM,需要下面的信息。

  1. data在Flash中的位置(flash_sdata)。
  2. data在RAM中的位置(ram_sdata)。
  3. .data段的大小(data_size)。

有了這些信息,可以使用以下代碼片段將數據從Flash復制到RAM。

        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

copy:
        ldrb  r4, [r0], #1
        strb  r4, [r1], #1
        subs  r2, r2, #1
        bne   copy

可以稍微修改鏈接腳本以提供這些信息。

Listing 10. Linker Script with Section Copy Symbols
SECTIONS {
        . = 0x00000000;
        .text : {
              * (.text);
        }
        flash_sdata = .; ❶

        . = 0xA0000000;
        ram_sdata = .; ❷
        .data : AT (flash_sdata) {
              * (.data);
        };
        ram_edata = .; ❸
        data_size = ram_edata - ram_sdata; ❹
}

❶ Flash中,data開始於所有code之后。

❷ RAM中,data開始於RAM的基址。

❸❹ 獲取數據的大小並不簡單。數據大小根據RAM中數據開始和結束時的差值計算。在鏈接器腳本中允許使用簡單的表達式。

從Flash復制數據到RAM的add程序如下所示。

Listing 11. Add Data in RAM (with copy)
        .data
val1:   .4byte 10               @ First number
val2:   .4byte 30               @ Second number
result: .space 4                @ 1 byte space for result

        .text

        ;; Copy data to RAM.
start:
        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

copy:
        ldrb  r4, [r0], #1
        strb  r4, [r1], #1
        subs  r2, r2, #1
        bne   copy

        ;; Add and store result.
        ldr   r0, =val1         @ r0 = &val1
        ldr   r1, =val2         @ r1 = &val2

        ldr   r2, [r0]          @ r2 = *r0
        ldr   r3, [r1]          @ r3 = *r1

        add   r4, r2, r3        @ r4 = r2 + r3

        ldr   r0, =result       @ r0 = &result
        str   r4, [r0]          @ *r0 = r4

stop:   b stop

程序使用Listing 10 Linker Script with Section Copy Symbols中列出的鏈接腳本進行匯編和鏈接。程序在Qemu中執行和測試。

qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
(qemu) xp /4dw 0xA0000000
a0000000:         10         30         40          0

Note

在具有SDRAM的實際硬件中,不應該立即訪問內存。在執行內存訪問之前,必須對內存控制器進行初始化。我們的代碼可以工作,因為模擬內存 不需要初始化內存控制器。

9.異常處理

到目前為止給出的示例有一個重大的錯誤。內存映射中的前8個words保留給異常向量。當異常發生時,PC指針被轉移到這8個位置之一。異常及其異常向量地址如下表所示。

Note

異常向量具體是幾個要看硬件架構,異常向量表是和具體硬件相關的。

Table 1. Exception Vector Addresses
Exception Address
Reset 0x00
Undefined Instruction 0x04
Software Interrupt (SWI) 0x08
Prefetch Abort 0x0C
Data Abort 0x10
Reserved, not used 0x14
IRQ 0x18
FIQ 0x1C

這些位置應該包含一個分支語句,該分支語句將跳轉至對應的異常處理程序。在我們目前看到的示例中,我們沒有在異常向量地址處插入分支指令。我們沒有遇到任何問題是因為這些異常沒有發生。通過將上述程序與以下匯編代碼鏈接,可以修復所有上述程序。

        .section "vectors"
reset:  b     start
undef:  b     undef
swi:    b     swi
pabt:   b     pabt
dabt:   b     dabt
        nop
irq:    b     irq
fiq:    b     fiq

只有reset異常向量被定向到和它自己標簽不同的地址start。所有其他異常向量都被定向到相同的標簽地址。因此,如果發生除reset之外的任何異常,處理器將在相同的位置死循環。然后可以通過調試器(在我們的例子中是monitor接口)查看pc的值來識別異常。

為了確保這些指令位於異常向量地址,鏈接器腳本應該如下所示。

SECTIONS {
        . = 0x00000000;
        .text : {
                * (vectors);
                * (.text);
                ...
        }
        ...
}

注意vectors段是如何放在其他代碼之前的,以確保vectors段位於從0x0開始的地址。

譯者注: 只有裸機程序才有加異常向量表的需求。在操作系統上跑的程序不用關心這些,它們由操作系統接管。

10.C語言啟動代碼

處理器剛復位時是不可能直接執行C代碼的。因為與匯編語言不同,C程序需要滿足一些基本的先決條件。本節將描述先決條件以及如何滿足這些先決條件。

我們將以sum of array的C程序為例。在本節結束時,我們將能夠執行必要的設置,將控制轉移到C代碼並執行它。

Listing 12. Sum of Array in C
static int arr[] = { 1, 10, 4, 5, 6, 7 };
static int sum;
static const int n = sizeof(arr) / sizeof(arr[0]);

int main()
{
        int i;

        for (i = 0; i < n; i++)
                sum += arr[i];
}

在將執行邏輯轉移到C代碼之前,必須正確設置以下內容。

  1. 全局變量
    1. 已初始化的
    2. 未初始化的
  2. 只讀數據

10.1.棧

C語言使用棧來存儲本地(自動)變量,傳遞參數,存儲返回地址等。所以在將控制權交給C代碼之前,棧必須正確設置。

棧在ARM架構中是非常靈活的,因為它完全由軟件實現。不熟悉ARM架構的可以看看概述附錄C.ARM的棧

為了確保不同編譯器生成的代碼具被互操作性(例如鏈接器的輸入目標文件可以是由不同的編譯器生成的),ARM創建了ARM Architecture Procedure Call Standard (AAPCS)。用作棧指針的寄存器,棧增長的方向,AAPCS中都有闡述。依據AAPCS,寄存器R13用作棧指針。棧也被規定是滿-遞減的。

放置全局變量和堆棧的一種方法如下圖所示。

Figure 5. Stack Placement

因此,在啟動代碼中所要做的就是將r13指向最高的RAM地址,這樣堆棧就可以向下增長(指向較低的地址)。對於connex板,可以使用以下ARM指令來實現。

ldr sp, =0xA4000000

注意,匯編程序為r13寄存器提供了一個別名sp。

Note

地址0xA4000000並沒有對應RAM,RAM結束地址是0xA3FFFFFF。但這沒關系,因為堆棧是滿遞減的,在第一次push期間堆棧指針將先遞減,變量值才被存儲。

10.2.全局變量

編譯C代碼時,編譯器將初始化的全局變量放在.data段。因此,與匯編一樣,.data必須從Flash復制到RAM。

C語言保證所有未初始化的全局變量都將初始化為零。當編譯C程序時,一個名為.bss的獨立段用於放置未初始化變量的描述。由於這些變量的值都是以0開頭的,所以它們不必存儲在Flash中。在將控件轉移到C代碼之前,必須將這些變量對應的內存位置初始化為零。

10.3.只讀數據

GCC將標記為const的全局變量放在一個名為.rodata的獨立段中。.rodata還用於存儲字符串常量。

由於.rodata段的內容不會被修改,所以可以放在Flash中。必須修改鏈接腳本以適應這種情況。

10.4.啟動代碼

現在我們知道了這些先決條件,就能創建鏈接腳本和啟動代碼。鏈接腳本Listing 10 Linker Script with Section Copy Symbols修改如下。

  1. .bss段排布
  2. verctors段排布
  3. .rodata段排布

內存中,.bss段位於.data段之后。.bss段的起始和結束符號也都在鏈接腳本中創建。Flash中.rodata段緊跟着.text段放置 。下圖展示了不同段的排布情況。

Figure 6. Section Placement

Listing 13. Linker Script for C code
SECTIONS {
        . = 0x00000000;
        .text : {
              * (vectors);
              * (.text);
        }
        .rodata : {
              * (.rodata);
        }
        flash_sdata = .;

        . = 0xA0000000;
        ram_sdata = .;
        .data : AT (flash_sdata) {
              * (.data);
        }
        ram_edata = .;
        data_size = ram_edata - ram_sdata;

        sbss = .;
        .bss : {
             * (.bss);
        }
        ebss = .;
        bss_size = ebss - sbss;
}

啟動代碼有如下幾個部分。

  1. 異常向量表
  2. 從Flash拷貝.data數據至RAM的代碼
  3. 清零.bss段的代碼
  4. 設置棧指針的代碼
  5. 跳轉至main的代碼
Listing 14. C Startup Assembly
        .section "vectors"
reset:  b     start
undef:  b     undef
swi:    b     swi
pabt:   b     pabt
dabt:   b     dabt
        nop
irq:    b     irq
fiq:    b     fiq

        .text
start:
        @@ Copy data to RAM.
        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

        @@ Handle data_size == 0
        cmp   r2, #0
        beq   init_bss
copy:
        ldrb   r4, [r0], #1
        strb   r4, [r1], #1
        subs   r2, r2, #1
        bne    copy

init_bss:
        @@ Initialize .bss
        ldr   r0, =sbss
        ldr   r1, =ebss
        ldr   r2, =bss_size

        @@ Handle bss_size == 0
        cmp   r2, #0
        beq   init_stack

        mov   r4, #0
zero:
        strb  r4, [r0], #1
        subs  r2, r2, #1
        bne   zero

init_stack:
        @@ Initialize the stack pointer
        ldr   sp, =0xA4000000

        bl    main

stop:   b     stop

要編譯代碼,不需要分別調用匯編器、編譯器和鏈接器。gcc足夠聰明,可以為我們一步完成。

如前所述,我們將編譯並執行Listing 12, Sum of Array in C所示的C代碼。

$ arm-none-eabi-gcc -nostdlib -o csum.elf -T csum.lds csum.c startup.s

-nostdlib選項指定不鏈接標准C庫。當鏈接到C庫時,需要額外注意一點。在11.使用C庫會討論。

符號表的轉儲可以更好地描述這些東西在內存中是如何放置的。

$ arm-none-eabi-nm -n csum.elf
00000000 t reset        ❶
00000004 A bss_size
00000004 t undef
00000008 t swi
0000000c t pabt
00000010 t dabt
00000018 A data_size
00000018 t irq
0000001c t fiq
00000020 T main
00000090 t start        ❷
000000a0 t copy
000000b0 t init_bss
000000c4 t zero
000000d0 t init_stack
000000d8 t stop
000000f4 r n            ❸
000000f8 A flash_sdata
a0000000 d arr          ❹
a0000000 A ram_sdata
a0000018 A ram_edata
a0000018 A sbss
a0000018 b sum          ❺
a000001c A ebss

reset,其余的異常向量從0x0開始放置。

❷ 匯編代碼位於8個異常向量之后(8 * 4 = 32 = 0x20)。

❸ 只讀數據n,放在Flash后的代碼中。

❹ 已初始化的數據arr(一個由6個整數組成的數組)位於RAM0xA0000000的開頭。

❺ 未初始化的數據sun放在已初始化的數據arr之后。(6 * 4 = 24 = 0x18)。

要執行程序,將程序轉換為.bin格式,在Qemu中執行,並轉儲位於0xA0000018sum變量。

$ arm-none-eabi-objcopy -O binary csum.elf csum.bin
$ dd if=csum.bin of=flash.bin bs=4096 conv=notrunc
$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
(qemu) xp /6dw 0xa0000000
a0000000:          1         10          4          5
a0000010:          6          7
(qemu) xp /1dw 0xa0000018
a0000018:         33

11.使用C庫

未完成

12.內聯匯編

未完成

13.貢獻

和其他開源項目一樣,我們很樂意接受貢獻。需要幫助的部分已經用FIXMEs標記。所有的貢獻將被適當地記入貢獻人員頁面。此文檔的源代碼保存在位於https://github.com/bravegnu/gnu-eprog。要對項目做出代碼貢獻,可以在github上fork項目並發送pull請求。

14.Credits

14.1.People

The original tutorial was written by Vijay Kumar B., vijaykumar@bravegnu.org Jim Huang, Jesus Vicenti, Goodwealth Chu, Jeffrey Antony, Jonathan Grant, David LeBlanc, reported typos and suggested fixes in the code and text.

14.2.Tools

The following great free software tools were used for the construction of the tutorial.

asciidoc for lightweight markup

xsltproc for HTML transformation

docbook-xsl for the stylesheets

highlight.js for syntax highlighting

dia for diagram creation

GoSquared Arrow Icons for the navigation icons

mercurial for version control

emacs

附錄

附錄A.ARM編程人員模型

本節提供了一個簡化的ARM編程人員模型。

寄存器檔案。在ARM處理器中,任何時候都有16個通用寄存器可用。每個寄存器的大小為32位。寄存器稱為Rn,其中n表示寄存器索引。所有指令對寄存器R0R13一視同仁。任何可以在R0上執行的操作都可以在寄存器R1R13上執行。但是R14R15由處理器分配特殊的功能。R15是程序計數器,包含要獲取的下一條指令的地址。R14是鏈接寄存器,用於在調用子例程時存儲返回地址。

Tip

盡管處理器未分配給R13特殊功能,但是按照慣例,操作系統用它作為棧指針,指向棧頂。

Current Program Status Register。當前程序狀態寄存器(CPSR)是一個專用的32-bit寄存器,包含下列幾個域。

  1. 條件標識
  2. 中斷掩碼
  3. 處理器模式
  4. 處理器狀態

在本教程提供的示例中,只使用condition flags字段。因此這里只闡述條件標志。

條件標志表示在執行算術和邏輯操作時發生的各種條件。下表給出了各種條件標志及其含義。

Table A.1. Condition Flags
標識 含義
進借位標識C 操作導致了進借位
溢出標識O 操作導致溢出
零標識Z 操作結果為0
負數標識N 操作結果為負數

附錄B.ARM指令集

ARM處理器有一個強大的指令集,但是這里只討論理解本教程中的示例所需的子集。

ARM有一個加載、存儲架構,這意味着所有算術和邏輯指令只接受寄存器操作數。它們不能直接對內存中的操作數進行操作。獨立的指令加載和存儲指令用於在寄存器和內存之間移動數據。

在本節中,將闡述以下這類指令

  1. 數據處理指令
  2. 分支指令
  3. 加載、存儲指令

數據處理指令。最常見的數據處理指令列在下表中。

Table B.1. Data Processing Instructions
指令 操作 例子
mov rd, n rd = n mov r7, r5 @ r7 = r5
add rd, rn, n rd = rn + n add r0, r0, #1 @r0 = r0 + 1
sub rd, rn, n rd = rn - n sub r0, r2, r1 @r0 = r2 + r1
cmp rn, n rn - n cmp r1, r2 @r1 - r2

默認情況下,數據處理指令不更新條件標志。如果指令的后綴是s,則指令將更新條件標志。例如,下面的指令添加兩個寄存器並更新條件標志。

adds r0, r1, r2
```https://www.adminiot.com.cn/
這個規則的一個例外是`cmp`指令。由於`cmp`指令的唯一目的是設置條件標志,所以它不需要`s`后綴來設置標志。

**分支指令**。分支指令導致處理器從不同的地址執行指令。有兩條分支指令可用:`b`和`bl`。除了分支,`bl`指令還將返回地址存儲在`lr`寄存器中,因此可以用於子例程調用。指令語法如下所示。

```asm
b label        @ pc = label
bl label       @ pc = label, lr = addr of next instruction

要從子例程返回,可以使用mov指令,如下所示。

mov pc, lr

條件執行。大多數其他指令集允許根據條件標志的狀態條件執行分支指令。在ARM中,幾乎所有指令都可以有條件地執行。

如果對應的條件為真,則執行該指令。如果條件為假,則將該指令轉換為nop。條件是通過在指令后面附加條件代碼助記符來指定的。

條件碼 助記符 條件 檢測的條件位
0000 EQ 相等/等於零 Z=1
0001 NE 不等 Z=0
0010 CS/HS 進位/無符號數大於等於 C=1
0011 CC/LO 無進位/無符號數小於 C=0
0100 MI 負數 N=1
0101 PL 正數或零 N=0
0110 VS 溢出 V=1
0111 VC 未溢出 V=0
1000 HI 無符號數大於 C=1 & Z=0
1001 LS 無符號數小於等於 C=0 or Z=1
1010 GE 有符號數大於等於 N=V
1011 LT 有符號數小於 N!=V
1100 GT 有符號數大於 Z=0 & N=V
1101 LE 有符號數小於等於 Z=1 or N!=V
1110 AL 總執行 任何狀態
1111 NV 從不(不要使用)

在下面的示例中,僅當進借位置位時,指令才將r1移動到r0。

MOVCS r0, r1

加載存儲指令。加載、存儲指令可用於在寄存器和內存之間移動單個數據項。指令語法如下所示。

ldr   rd, addressing    ; rd = mem32[addr]
str   rd, addressing    ; mem32[addr] = rd
ldrb  rd, addressing    ; rd = mem8[addr]
strb  rd, addressing    ; mem8[addr] = rd

addressing是由兩部分組成的

  • 基址寄存器
  • 偏移

基址寄存器可以是任何通用寄存器。偏移寄存器和基址寄存器可以以三種不同的方式交互。

  1. 偏移
  • 從基址寄存器中添加或減去偏移量以形成地址。ldr示例:ldr rd, [rm, offset]
  1. 前變址
  • 從基址寄存器中添加或減去偏移量以形成地址,尋址完成后將地址寫回基址寄存器。ldr示例:ldr rd, [rm, offset]!
  1. 后變址
  • 基址寄存器包含要訪問的地址,尋址完成后將基址址中添加或減去偏移量並存儲在基寄存器中。ldr示例:ldr rd, [rm], offset

偏移量可以采用以下格式

  1. 立即數
  • 偏移量是一個無符號數,它可以與基址寄存器的值相加或相減。常用於訪問棧中的結構成員、局部變量。立即數以#開頭。
  1. 寄存器
  • 偏移量是通用寄存器中的一個無符號值,它可以與基址寄存器的值相加或相減。常用於訪問數組元素。

一些示例

ldr  r1, [r0]              ; same as ldr r1, [r0, #0], r1 = mem32[r0]
ldr  r8, [r3, #4]          ; r8 = mem32[r3 + 4]
ldr  r12, [r13, #-4]       ; r12 = mem32[r13 - 4]
strb r10, [r7, -r4]        ; mem8[r7 - r4] = r10
strb r7, [r6, #-1]!        ; mem8[r6 - 1] = r7, r6 = r6 - 1
str  r2, [r5], #8          ; mem32[r5] = r2, r5 = r5 + 8

附錄C.ARM的棧

堆棧在ARM體系結構中非常靈活,因為完全由軟件實現。

棧指令。ARM指令集不包含任何特定的棧指令,比如pushpop。指令集也不強制使用堆棧。pushpop操作由內存訪問指令執行,具有自動增量尋址模式。

棧指針。棧指針是指向棧頂部的寄存器。在ARM處理器中,沒有專用的棧指針寄存器,任何通用寄存器都可以用作堆棧指針。

棧類型。由於棧是由軟件來實現的,不同的實現選擇會產生不同類型的棧。根據棧的增長方式,有兩種類型的棧。

  • 遞增棧

在push操作時棧指針增加,例如棧向更高的地址增長。

  • 遞減棧

在push操作時棧指針遞減,例如棧向更低的地址增長。

根據棧指針指向的內容,又可以將棧分為兩類。

  • 空棧

棧指針指向的位置用於存儲下一個存儲對象。push操作先在當前位置存儲這個值,然后棧指針自增。

  • 滿棧

棧指針指向的位置是最后一個存儲對象。push操作先讓棧指針自增,然后在新的位置存儲這個值。

一共可以實現四種不同類型的棧——滿遞增、滿遞減、空遞增和空遞減。這四種類型的棧都可以使用寄存器加載存儲指令來實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM