GNU工具链编写嵌入式程序

来源:互联网 发布:巡洋舰 驱逐舰 知乎 编辑:程序博客网 时间:2024/05/07 14:24

序言

书写启动代码和链接脚本是一个只被少数人精通的黑魔法,文档的缺少导致这部分人更少。通过这个系列文章的学习,希望能够帮助读者解开这些谜团,揭开底层代码的神秘的面纱,同时希望能够更多人加入这个少数精英的行列。

对于firmware和系统移植者,这些知识会帮助他们移植软件到微处理芯片上,同时这些知识也会帮助到bootloader的开发者和内核启动早期的开发者。

需要你拥有的技能:
- 熟悉C语言的基本原理
- 熟悉微处理器
- 熟悉linux命令行

硬件和软件需求:

Windows / Linux 笔记本电脑
Qemu (>= 1.5) 模拟目标板
VirtualBox (>= 4.1.18) (支持磁盘镜像)


GNU工具链编写嵌入式程序

Fork me on github

作者:Vijay Kumar B.
邮箱:vijaykumar@bravegnu.org
历史版本

1 介绍

GNU的工具链越来越多的被用在嵌入式软件开发中。这种软件也被称为单机C程序或者裸机C程序。裸机C程序引入了一系列的问题,就是需要深入的理解GNU工具链。GNU工具链手册提供了很多信息在工具链上,但是从工具链的透明度来讲,依然有透明度的问题。所以,如何假定去写手册就是个问题。所以问题是导致手册相当凌乱,很多工具链的新手需要忍受不了这种折磨。
这个手册的目标是通过解释透明度问题,在这些缝隙间架起一座桥梁。希望能够帮助越来越多的开发者在嵌入式项目中使用GNU工具链。
对于这个指导,基于ARM的嵌入式环境使用Qemu, 你不需要投资硬件,可以通过你的个人pc轻松的学习GNU的工具链。这个指导手册本身不教授ARM指令集,你可以从其他一些在线指导学习:
- ARM Assembler
- ARM Assembly Language Programming

但是为了方便读者,我们把高频的ARM指令在后边列出来。


2 设置ARM实验室

本节学习如何使用QEMU和GNU工具链,在PC上搭建ARM开发和测试环境。QEMU是一个硬件模拟器可以模拟多种机器,包括基于ARM的机器。你可以写ARM汇编程序,使用GNU工具链编译他们,并在Qemu上运行和测试。

2.1 Qemu ARM

Qemu模拟一个来源于Gumstix基于connex板的PXA255。PXA255有一个ARMv5TE指令集的ARM核,PXA255也有很多外围设备,本指导会讲解一些外围设备。

2.2 dibian系统安装Qemu

需要安装0.9.1以上版本的qemu,ubuntu和dibian自带的apt-get可以安装比较新的qumu。

$ apt-get install qemu

2.3 安装GNU ARM工具链

  • CodeSourcery已经带了支持多种架构的GNU的工具链。 下载GNU的工具链在
    http://www.mentor.com/embedded-software/sourcery-tools/sourcery-codebench/editions/lite-edition/
  • 解压缩到~/toolchains
$ mkdir ~/toolchains$ cd ~/toolchains$ tar -jxf ~/downloads/arm-2008q1-126-arm-none-eabi-i686-pc-linux-gnu.tar.bz2
  • 添加工具链环境变量到.bashrc
PATH=$HOME/toolchains/arm-2008q1/bin:$PATH

3 HELLO ARM

本段,我们学习写ARM的汇编程序,并且使用qemu模拟 connex 裸板测试。

汇编语言的源程序由一些列的语句组成,每行一条语句。每条语句有如下格式:

label:    instruction         @ comment

每个组件是可选的。

label
标签是一个方便去定位内存中指令的位置的方式。标签可以使用任何地址去替换,标签由字母,数字,_,和$组成。

comment
注释由@开始,@后边的字符将会被在汇编中忽略。

instruction
instruction是Arm的指令或者汇编指令,汇编指令是命令汇编器的指令,通常以.开始。

这是一个很简单的汇编指令,两个数的累加:

        .textstart:                       @ 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 r2stop:   b stop               @ Infinite loop to stop execution

.text是一个汇编指令,它是指下边所有的代码编译到.text代码段中,而不是.data代码段中。

3.1 创建二进制

保存上边的代码到add.s中,为了汇编上边的代码需要使用GNU工具链中的汇编器as。

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

-o 选项指定输出文件名

note:
为了避免和本机的工具链重名冲突,交叉工具链通常带目标架构的前缀。为了增加可读性,本文中不带前缀。

为了生成可执行文件,需要调用GNU工具链中的链接器ld。

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

在这里-o选项同样是指定输出文件名。参数-Ttext=0x0,指定地址被分配到的标号, 因此指定的起始地址是0x0。nm命令可以看到多个标签分配的地址。

$ arm-none-eabi-nm add.elf... clip ...00000000 t start0000000c t stop

地址分配给标签start和stop, start分配的是0x0, 因为它是第一个指令,stop分配的是前3条指令后边,每个指令占4字节,因此stop被分的地址是12(0xc)。

一个不一样的基地址将会把这些标签分配一组不同的地址。

$ arm-none-eabi-ld -Ttext=0x20000000 -o add.elf add.o$ arm-none-eabi-nm add.elf... clip ...20000000 t start2000000c t stop

ld创建的输出文件使用ELF格式。有很多文件格式是可执行的。当你有个操作系统ELF格式可以很好的工作,但是因为要运行程序到裸机,我们必须把它转化成二进制文件格式。

一个二进制文件包含从一个指定的内存地址连续的字节,没有附加的信息装载到文件中,这对Flash程序工具很方便,因为当所有的所有的程序文件拷贝到Flash上,可以从一个指定的连续的内存地址开始启动。

GNU工具链的objdump命令能够转化不同目标文件格式。一个通用的用法如下:

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

为了把add.elf转化成二进制文件可以按照下面的命令。

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

检查文件的尺寸大小。这个文件只有16字节。因为只有4条指令,每个指令占4字节。

$ ls -al add.bin-rw-r--r-- 1 vijaykumar vijaykumar 16 2008-10-03 23:56 add.bin

3.2 在qemu中执行

当ARM处理器重启,将会从地址0x0开始。在connex板上有一个16MB的Flash,地址从0x0开始。这个Flash的当前指令将会被执行。

当qemu模拟connex板,必须指定一个文件模拟FLASH内存。FLASH文件格式非常简单。为了得到Flash的地址X上的字节, qemu读取这个文件偏移到Xbyte上。实际上,这和二进制文件很相似。

为了测试这个程序,在模拟Gumstix connex board上,我们创建一个16MB的文件代表FLASH,我们使用dd命令从/dev/zero到文件flash.bin拷贝16MB的零。这个数据使用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的内存中。
然后重启,处理器将会从0开始执行。可以调用下面的qemu的指令。

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

-M connex选项指定模拟的机器是connex。
-pflash选项指定文件flash.bin代替Flash内存。
-nographic指定模拟器不需要图形显示器。
-serial /dev/null 指定connex的串口连接到/dev/null,因此串口的数据被丢弃。

系统执行指令集,当执行完后,保持到无限循环stop: b stp指令。为了观察寄存器的内容,可以使用qemu的监控接口。监控接口是一行命令,可以看到模拟系统的控制和系统状态。当qemu开始上边的命令,监控接口提供标准的输入/输出。

为了查看几春秋的内容,可以使用info registers命令。

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

注意寄存器R02的值。这个寄存器包含了累加的结果,是我们期望的9。

3.3 更多的监控命令

下面列举一些有用的监控指令。

命令 功能 help 列举可用的命令 quit 退出模拟器 xp /fmt addr 导出从addr开始的物理内存地址 system_reset 系统重启

xp 命令需要做一下说明。fmt参数指定显示多少内容。语法是 。

count
指定显示数据对象个数
size
指定每个数据对象大小。 b for 8 bits, h for 16 bits, w for 32 bits and g for 64 bits.
format
指定格式,x是16进制,d是带符号的10进制,u是不带符号的10进制,o是8进制,c是字节,i是asm指令。

xp 命令带了i的格式,可以用来反汇编当前内存的指令。xp 命令指定参数为4iw,从地址0x0开始反汇编,4表示显示4条记录,i指定记录需要打印为汇编指令,w指定每个记录32位大小。下面是命令的输出。
The 4 specifies 4 items are to be displayed, i specifies that the items are to be printed as

(qemu) xp /4iw 0x00x00000000:  mov        r0, #5  ; 0x50x00000004:  mov        r1, #4  ; 0x40x00000008:  add        r2, r1, r00x0000000c:  b  0xc

4 更多的汇编指令

本节,我们使用2个实例程序描述一些通用的汇编指令。
1. 累加一个数组
2. 计算字符串的长度

4.1 累加数组

下面的程序累加数组的值保存到r3。

   .textentry:  b start                 @ Skip over the dataarr:    .byte 10, 20, 25        @ Read-only array of byteseoa:                            @ Address of end of array + 1        .alignstart:        ldr   r0, =eoa          @ r0 = &eoa        ldr   r1, =arr          @ r1 = &arr        mov   r3, #0            @ r3 = 0loop:   ldrb  r2, [r1], #1      @ r2 = *r1++        add   r3, r2, r3        @ r3 += r2        cmp   r1, r0            @ if (r1 != r2)        bne   loop              @    goto loopstop:   b stop

下面描述这个程序引入的两个新的指令.byte和.align。

4.1.1 .byte

汇编器把.byte的字节大小的参数组装连续字节到内存中,这里还有2个类似的命令 .2byte 和 .4byte,相对的分别保存16位和32位的值。下边给出通用的语法。

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

这些参数可以是简单的整形字符,也可以是二进制(0b或0B为前缀),8进制(o为前缀),10进制或16进制(0x或0X为前缀)的数值。整数可以被表示为字符常量(单引号的字符),这种情况下,使用字符的ACSII值。

这些参数也可以由C表达式构造成,如下面的例子。

pattern:  .byte 0b01010101, 0b00110011, 0b00001111npattern: .byte npattern - patternhalpha:   .byte 'A', 'B', 'C', 'D', 'E', 'F'dummy:    .4byte 0xDEADBEEFnalpha:   .byte 'Z' - 'A' + 1

4.1.2 .align

ARM需要指令按照32位长度对齐。在一个4字节的指令中,第一个指令的地址应该是4的整数倍。为了保持这个原则,.align指令可以被用来插入一些填充位直到下一个字节的地址是4的整数倍。只有当数据或者半字被插入到代码的时候,这是必须的。

4.2 字符串长度

        .text        b startstr:    .asciz "Hello World"        .equ   nul, 0        .alignstart:  ldr   r0, =str          @ r0 = &str        mov   r1, #0loop:   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 -= 1stop:   b stop

本段代码介绍2个汇编指令 .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

FLash内存, 在前一个例子中存储程序, 是一种EEPROM(电可擦只读存储器)。它是一种有用的二级存储,像硬盘一样,但是不方便保存变量到FLASH中。变量应该保存到RAM中,以便能够简单的修改。

connex板有RAM有64M,地址从0xA0000000开始,可以存储变量。connex板的内存的映射图可以使用下面的图片表示。

RAM是随机存储器,掉电丢失数据。

CONNEX RAM
把变量放置到这个地址上,必须完成启动加载。为了理解怎么完成的,必须理解汇编器和连接器。

6 链接器

当写一个多文件的程序,每个文件被汇编成为单独的目标文件,连接器把这些目标文件组织成一个最终可以执行的文件。

链接器

当把多个目标文件混合在一起, 链接器执行下面的操作。

  • 符号分辨
  • 重定位
    本节我们调查这些操作的细节。

    6.1 符号

    单文件程序中,当处理一个目标文件,汇编器把涉及到的所有标号使用它们对应的地址替换。但是在多文件程序中,如果任何标号如果定义在其他文件中,汇编器会把标识它是“未识别”,当这些目标文件传递给链接器,链接器从其他目标文件中确定这些值,并且使用正确的值修正代码。

数组的和的例子被查分到2个文件,为了说明通过链接器符号分辨被执行。这两个文件将会汇编并且他们的符号表检查显示当前未辨别的标识符。

sum-sub.s 包含子程序,并且文件main.s调用子程序。

/* main.s */       .text        b start                 @ Skip over the dataarr:    .byte 10, 20, 25        @ Read-only array of byteseoa:                            @ Address of end of array + 1        .alignstart:        ldr   r0, =arr          @ r0 = &arr        ldr   r1, =eoa          @ r1 = &eoa        bl    sum               @ Invoke the sum subroutinestop:   b stop
/* sum-sub.s */      @ Args        @ r0: Start address of array        @ r1: End address of array        @        @ Result        @ r3: Sum of Array        .global sumsum:    mov   r3, #0            @ r3 = 0loop:   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.o00000004 t arr00000007 t eoa00000008 t start00000018 t stop         U sum$ arm-none-eabi-nm sum-sub.o00000004 t loop00000000 T sum

当前,关注第二列的字母,它指定了符号的类型。t标识符号是已经被定义。u指示符号未被定义。符号是.global的,字母需要大写。

很明显的是符号sum被定义在sum-sub.o中,并且在main.o中没有被识别。当链接器调用这些符号将会被识别,同时生成可执行文件。

6.2 重定位

重定位是一个改变已经分配标签地址的过程。这将会引起相关的标签重新映射到新分配的地址。首先,重定位有下面2个原因:
1. 段落合并
2. 段落布局

为了理解这个重定位的过程,对段概念的理解是基本的。

代码和数据有不同的运行时间需求。例如代码可以被放置在只读内存区,数据需要放在可读写的内存区。如果代码和数据没有交叉,这样会很方便。为了这个目标,程序分成不同的段。大多数程序有至少2个段落, .text代码段和.data数据段。汇编指令.text和.data,用来转换2个段落的后边和前边。

可以把每个段想象成一个桶。当汇编器切割一个段指令,分别把代码/数据的指令放在选择的桶中。因此代码和数据属于一个特殊的部分出现在连续的地址上。下面的图片显示了汇编器如何重新安排数据段。

段落

现在,我们理解了段,让我们继续调查重定向的根因。

6.2.1 段合并

当处理多段程序时,有着相同名字的段落会出现在每个文件中。链接器负责合并输入文件中的多个段落到输出文件。默认地,有着相同的名字,来自于不同文件的段落被连续放置,并且相关的标签被修正映射到新的地址上。

段落的合并影响可以观察目标文件的符号表和相关的可执行文件。这个多个文件累加数字的程序可以用来说明段落合并。目标文件main.o和sum-sub.o的符号表,以及可执行文件sum.elf的符号表在下:

$ arm-none-eabi-nm main.o00000004 t arr00000007 t eoa00000008 t start00000018 t stop         U sum$ arm-none-eabi-nm sum-sub.o00000004 t loop00000000 T sum$ arm-none-eabi-ld -Ttext=0x0 -o sum.elf main.o sum-sub.o$ arm-none-eabi-nm sum.elf...00000004 t arr00000007 t eoa00000008 t start00000018 t stop00000028 t loop00000024 T sum

❶ ❷ loop符号有地址0x4在sum-sub.o中,和0x28在sum.elf中。因为sum-sub.o的.text段被替换到main.o的.text段后边。

The loop symbol has address 0x4 in sum-sub.o, and 0x28 in sum.elf, since the .text section of sum-sub.o is placed right after the .text section of main.o.

6.2.2 段放置

当一个程序别汇编,假定每个段的起始地址都是0。并且因此符号都是分配相对于段落起始处的值。当最后创建可执行程序时,段落被分配到X地址,并且段落相关定义的标签,通用增长X, 因此指向新的位置。

每个段落的布局在内存中是独有的一个位置,并且链接器把相关的放置这个段落标签都修正。

段落重置的盈盈可以通过观察目标文件的符号表和相关的可执行文件。单文件累计数字成像的和可以用来说明这个重置。为了使描述更清晰,我们把.text段放在0x100地址上。

$ arm-none-eabi-as -o sum.o sum.s$ arm-none-eabi-nm -n sum.o00000000 t entry ❶00000004 t arr00000007 t eoa00000008 t start00000014 t loop00000024 t stop$ arm-none-eabi-ld -Ttext=0x100 -o sum.elf sum.o ❷$ arm-none-eabi-nm -n sum.elf00000100 t entry ❸00000104 t arr00000107 t eoa00000108 t start00000114 t loop00000124 t stop...

❶ 标签的地址以0地址开始。
❷可执行文件创建,链接器构造本段落的地址在0x100
❸在.text断种,标签的地址别重新分配,以0x100开始,并且所有的相关标签都会修正映射从这个地址。

段落的合并和重置展示在下图中。
重定位

7 链接器脚本文件

在前一节中提到,段落的合并和重置是链接器完成的。程序员可以通过链接脚本控制段落如何被合并,并且指定内存中的位置。一个非常简单 的脚本文件,如下。

SECTIONS { ❶        . = 0x00000000; ❷        .text : { ❸                abc.o (.text);                def.o (.text);        } ❹}


SECTIONS 命令是最重要的链接器命令,它指定这些段落如何合并和他们放到什么位置。

在SECTIONS命令中. 代表位置计数器。这个位置通常被初始化成0x0.它可以通过一个新值来改变。设置值0x0为开始是多余的。

❸ ❹
脚本的这个部分特指,输入文件abc.o和.def.o 的.text段应该到达输出文件.text段。

链接脚本可以通过应用通配符*替换个别指定文件名进一步简化。

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

如果程序同时包含.text和.data段,.data段合并和重定向可以别如下指定。

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

这里,.text段分配到0x0地址,.data段分配到0x400。如果一个位置计数器没有分配一个不同的值,.text和.data段将会被分配到临近的内存位置。

7.1 链接脚本举例

为了说明这个脚本,我明使用链接脚本在如上,去控制程序.text和.data部分的位置。我明将会使用一个轻量的数组求和的修改版本展示。代码如下。

       .dataarr:    .byte 10, 20, 25        @ Read-only array of byteseoa:                            @ Address of end of array + 1        .textstart:        ldr   r0, =eoa          @ r0 = &eoa        ldr   r1, =arr          @ r1 = &arr        mov   r3, #0            @ r3 = 0loop:   ldrb  r2, [r1], #1      @ r2 = *r1++        add   r3, r2, r3        @ r3 += r2        cmp   r1, r0            @ if (r1 != r2)        bne   loop              @    goto loopstop:   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.elf00000000 t start0000000c t loop0000001c t stop00000400 d arr00000403 d eoa

从这个符号表中,明显的看到.text以地址0x0放置,.data段放置到了以0x400开始的地址上。

8 RAM中的数据,举例

我们现在已经知道了如何写链接脚本,我们将写一个程序,把数据放到RAM中。

修改加法程序从RAM中加载两个值,累加它们并存储在RAM上,这两个值和结果被放置到.data段中。

        .dataval1:   .4byte 10               @ First numberval2:   .4byte 30               @ Second numberresult: .4byte 0                @ 4 byte space for result        .text        .alignstart:        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 = r4stop:   b stop

链接脚本如下:

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

.elf的符号表如下

$ arm-none-eabi-nm -n add-mem.elf00000000 t start0000001c t stopa0000000 d val1a0000001 d val2a0000002 d result

这个链接脚本表面上看似乎解决了把数据放到RAM上的问题,稍微等下,这个解决方案并不完整。

8.1 RAM是不稳定

RAM是不稳定的内存,所以上电后不能直接把数据放到RAM上。

所有的数据和代码都要存放到FLASH上。启动后,支持把FLASH上的数据拷贝到RAM上,然后才开始执行程序。数据段有2个地址,FLASH的加载地址和RAM运行地址。

在ld中,加载地址被称为LMA,运行地址被称为VMA。

为了使程序正确执行,必须完成下面两步操作:
1. 对于.data段,链接脚本需要同时指定加载地址和运行地址。
2. 有一小段代码把.data段从FLASH上拷贝到RAM上。

8.2 指定加载地址

运行地址应该由标签的地址确定。在前面的脚本,我们指定了.data段的运行地址。装载地址不明确,默认加载到了运行地址。前面的例子,程序开始在FLASH上执行。但是如果在RAM运行期间数据段被放进去了,数据段的在FLASH上的加载地址,必须对应于RAM的运行地址。

加载地址不同于运行地址,可以有AT关键字指定。修改的链接脚本如下:

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


在SECTION命令中,可以通过设置值来创建符号。这里etext被分配到局部计数器的值所在的位置。etext包含FLASH上所有代码代码后下一个空闲区域的地址。这将会指定.data段在FLASH的地址。注意,etext本事没有分配内存,它只是一个符号表的入口。


AT关键字指定了.data段的加载地址。一个地址或者符号可以当做参数被传递给AT。这里在FLASH上所有的代码段后被指定位.data的加载地址。

8.3 copy数据到RAM

为了拷贝数据段到RAM, 需要下面的信息:
1. FLASH上.data段的地址。
2. RAM上.data的地址。
3. .data段的大小。

如果包含上面的信息,就可以使用下面的代码把数据段代码拷贝到RAM上。

        ldr   r0, =flash_sdata        ldr   r1, =ram_sdata        ldr   r2, =data_sizecopy:        ldrb  r4, [r0], #1        strb  r4, [r1], #1        subs  r2, r2, #1        bne   copy

为了支持这些信息,链接脚本需要被轻微的修改。

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


在FLASH上,数据段的开始是在所有代码后边。


RAM上,数据段的开始。

❸ ❹
间接获取数据的尺寸。数据的大小等于RAM上数据段的结束位置和起始位置相减。是的,在链接脚本中允许简单的表达式。

        .dataval1:   .4byte 10               @ First numberval2:   .4byte 30               @ Second numberresult: .space 4                @ 1 byte space for result        .text        ;; Copy data to RAM.start:        ldr   r0, =flash_sdata        ldr   r1, =ram_sdata        ldr   r2, =data_sizecopy:        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 = r4stop:   b stop

可以使用上边的链接脚本汇编和链接。在Qemu中执行代码:

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

9 异常处理

例子中给了一个重要的bug,在内存地图中开始的8个字被预留给异常向量。当一个异常发生,会传送到这8个区中。这个异常和他们的异常向量地址如下表。

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     startundef:  b     undefswi:    b     swipabt:   b     pabtdabt:   b     dabt        nopirq:    b     irqfiq:    b     fiq

只要重启异常指向以一个不同的地址start。其他所有的异常都跳转到相同地址。因此,如果任何异常导致reset,处理器将会在同样的位置循环。这个异常可以通过调试器查看pc的值。

为了确保这些指令被放置到一样向量的地址,链接脚本通常应该如下。

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

通知所有的其他代码前面的vectors段落如何放置,确保vectors以地址0x0开始。

10 C 启动

当程序重新启动,不能直接执行C代码。因为,和汇编不同,C语言需要一些预设值。本节主要描述预设值和如何实现。

我们拿数组相加的C语言的例子说明。我们需要完成启动,并且转换到可以执行C程序,并开始执行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. 堆栈
2. 全局变量
- 初始化
- 未初始化
3. 只读数据

10.1 堆栈

c语言使用堆栈存储局部变量,传递函数参数,等等。因此在转化到c代码,需要真确启动的核心是堆栈。

在ARM架构中,堆栈相当灵活,可以不需要软件完整实现。对于不熟悉ARM架构的人来说,可以参见后边的参考资料:备注3 ARM堆栈。

为了保证不同的编译器无差别的创建代码,ARM公司创建了ARM架构程序调用标准(AAPCS)。AAPCS中,寄存器被用来作为栈指针和所有的栈的增长。遵从AAPCS,寄存器R13用来作为栈指针,同时被指向栈底。

下面是一个全局变量和栈的位置:
栈

因此程序启动后,R13指向RAM的最高地址。然后随着栈的增长递减。contex单板可以用下面的命令实现。

  ldr sp, =0xA4000000

汇编中,sp是r13的别名。

0xA4000000地址并不存在,RAM的结束地址是0x3F000000. 但是这是正确的,因为栈是全递减的,当栈压栈的时候,R13保存第一个值。

10.2 全局变量

10.3 只读数据

gcc会把标记为const的全局变量作为一个单独的段,被称为.rodata。.rodata用来存放固定的字符串。因为.rodata不需要修改,所以它可以保存在FLASH上。链接脚本需要适应。

10.4 启动代码

现在我们知道了在链接脚本和启动代码前的必须项。链接脚本必须满足下边几项:
1. bss段的放置
2. vectors段的放置
3. .rodata段的放置
在RAM中.bss段结束后放置.data段。链接脚本中.bss的起始都需要定位。Flash上.rodata段放置在.text段后。下图描述了段的关系。
c的程序段

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. 将.data从FLASH拷贝到RAM
  3. 初始化.bss
  4. 栈指针启动
  5. 切换到main
        .section "vectors"reset:  b     startundef:  b     undefswi:    b     swipabt:   b     pabtdabt:   b     dabt        nopirq:    b     irqfiq:    b     fiq        .textstart:        @@ 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_bsscopy:        ldrb   r4, [r0], #1        strb   r4, [r1], #1        subs   r2, r2, #1        bne    copyinit_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, #0zero:        strb  r4, [r0], #1        subs  r2, r2, #1        bne   zeroinit_stack:        @@ Initialize the stack pointer        ldr   sp, =0xA4000000        bl    mainstop:   b     stop

编译代码,它再不需要单独的调用汇编器,链接器和编译器。gcc把它们集成到了一起。

我们用这个约定,执行一下之前的c代码程序。

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

-nostdlib 选项指定c的标准库不会编译进来。

符号表说明了内存中如何存放。

$ arm-none-eabi-nm -n csum.elf00000000 t reset        ❶00000004 A bss_size00000004 t undef00000008 t swi0000000c t pabt00000010 t dabt00000018 A data_size00000018 t irq0000001c t fiq00000020 T main00000090 t start        ❷000000a0 t copy000000b0 t init_bss000000c4 t zero000000d0 t init_stack000000d8 t stop000000f4 r n            ❸000000f8 A flash_sdataa0000000 d arr          ❹a0000000 A ram_sdataa0000018 A ram_edataa0000018 A sbssa0000018 b sum          ❺a000001c A ebss


重启和异常向量的地址 0x0.


汇编代码的开始是8个异常向量后 (8 * 4 = 32 = 0x20).


FLASH上只读数据n在代码段后.


初始化数据arr, 6个整形的数组,放在了RAM的开始0xA0000000.


未初始化数据sum放在RAM的6个整数后. (6 * 4 = 24 = 0x18)

为了执行这个代码,需要将程序转换为.bin格式,在Qemu中,打印sum的地址是0xA0000018.

$ 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 0xa0000000a0000000:          1         10          4          5a0000010:          6          7(qemu) xp /1dw 0xa0000018a0000018:         33

11 使用C库

12 行内汇编

13 贡献

像其他开源软件一样,我们乐意接受各种贡献。需要帮助的部分已经标记为FIXME。所有的贡献者会被加入到人员列表中。

这个文档是在一个公共仓库上, 地址是https://github.com/bravegnu/gnu-eprog。为了贡献这个项目,需要拉取分支,并发送pull request.

本文使用asciidoc写的,并且使用docbook-xsl格式转化。

14 工作列表

14.1. 人员

最原始的指导书的作者是Vijay Kumar B., <vijaykumar@bravegnu.org>.Jim Huang, Jesus Vicenti, Goodwealth Chu, Jeffrey Antony, Jonathan Grant, David LeBlanc, 报告代码和文本中的错误,提供修复建议。

14.2. 工具

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

asciidoc 轻量级的标记语言xsltproc html的格式转化docbook-xsl 模板highlight.js 语法高亮dia 创建图片GoSquared Arrow Icons 导航图片mercurial 版本控制emacs 编辑器

15 版权

“Embedded Programming with the GNU Toolchain” is Copyright © 2009, 2010, 2011 Vijay Kumar B. vijaykumar@bravegnu.org

This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.


备注1:ARM程序模型

备注2:ARM指令集

备注3:ARM堆栈

0 0