.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的。
很明显符号sum在sum-sub.o中定义了,不过在main.o中还未解析。当链接器被调用后,符号引用被解析,然后生成可执行文件。
6.2.重定位
重定位是改变标签已分配地址的过程。这还包括调整所有标签的引用地址以使对应上最新分配的地址。重定位主要基于以下两个原因:
段合并
段排布
要理解重新定位的过程,理解段的概念是必不可少的。
代码和数据有不同的run-time需求。例如代码可以放置在只读的存储介质中,数据则要放置在读-写存储介质中。如果代码和数据不混在一起,也许会更合适。为此,程序被分割为段。大多程序至少包含2个段,.text段放代码,.data段放数据。汇编器指令.text和.data用于在这两个段间来回切换。
可以把每个段想象成一个桶。当汇编器识别到一个段指令时,它会把紧跟指令的代码/数据放到对应的桶里面。因此,属于特定段的代码/数据的位置是紧挨着的。下面的图显示了汇编器如何将数据重新排列到段中。
Figure 3. Sections

现在我们已经了解了段,让我们看看执行重定位的主要原因。
6.2.1.段合并
当处理多源文件程序时,在每个文件中可能会出现同样名字的段(例如.text)。链接器负责将输入文件的段合并到一起放到输出文件对应的段中。默认情况下,拥有同样名字的段会被放置到一起,标签引用的地址也会被调整到对应的新的地址上。
通过查看目标文件和相应的可执行文件的符号表,可以看到段合并的效果。多源文件的sum of array程序可以用来阐明段合并。目标文件main.o、sum-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.o、def.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个修改。
链接器需要同时指定.data段的加载地址和运行地址。
一段用于将.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,需要下面的信息。
data在Flash中的位置(flash_sdata)。
data在RAM中的位置(ram_sdata)。
.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);
上一篇:s3c2440调试nandflash裸机程序遇到的问题
下一篇:MMU 和 MPU的区别
推荐阅读最新更新时间:2026-02-25 12:44
- ADRF6704-EVALZ,用于评估具有 2500 至 2900 MHz Frac-N PLL 和集成 VCO 的 ADRF6704 2050 至 3000 MHz 正交调制器的评估板
- AM2G-4815SH30Z 15V 2 瓦 DC/DC 转换器的典型应用
- AM1D-1209SH30-RZ 9V 1W DC/DC 转换器的典型应用
- 使用 Analog Devices 的 LTC3207 的参考设计
- 具有 BLF881 的 174MHz 至 230MHz DVB-T 功率放大器应用电路
- LTC2992HMS 功率效率计的典型应用
- Pixelblaze V3:支持 Wi-Fi、可实时编码的 LED 控制器,具有基于 Web 的开发环境
- 使用 Diodes Incorporated 的 AP5002 的参考设计
- 具有关断功能的 LTC1403A-1 串行 14 位、2.8 Msps 采样 ADC 的典型应用
- MAXREFDES1010:24V / 300mA,无光隔离反激式DC-DC转换器



背包客_Follow me 第三季第4期
Azure RTOS ThreadX内核用户手册,含SMP多核(中文版)
非常经典的关于LLC的杨波博士论文
ASM10DTBD-S664






京公网安备 11010802033920号