什么是可重定位和绝对机器码?

22

在学习汇编语言时,我遇到了这些术语。我的理解是,在可重定位的机器代码中,代码不依赖于静态RAM位置。汇编器为程序指定RAM需求。内存可以放置在链接器找到的任何空间。

这个理念正确吗?如果是,汇编器是如何实现的?

还有,一些绝对机器代码的例子是什么?


1
“内存可以放置在链接器找到空间的任何地方。”这句话表述得非常糟糕。通过“内存”,您可能指的是代码和数据,而不是写入和读取的内存。通过“链接器”,您可能指的是加载器,系统实用程序将代码和数据从存储设备传输到内存中。该加载器可能具有重定位功能,以及用于使用共享代码和库的链接器功能。链接器通常用于“编译”或构建的最后阶段,当对象文件合并为单个可执行映像时。 - sawdust
4个回答

41
许多/大多数指令集都具有基于程序计数器的相对寻址,这意味着获取正在执行的指令地址相关的程序计数器地址,然后添加偏移量并将其用于访问内存或分支等操作。这就是您所说的可重定位性。因为无论指令在地址空间中的位置如何,您想要跳转到的内容都是相对的。将整个代码和数据块移动到其他地址,它们仍然相对距离相同,因此相对寻址仍将起作用。如果相等,则跳过下一条指令可以在任何地方使用(跳过if、被跳过的指令以及跳过之后的指令)。
绝对寻址使用绝对地址,跳转到精确地址,从精确地址读取。如果相等,则分支到0x1000。
汇编器不执行此操作,而是由编译器和/或程序员执行。通常,最终编译代码将具有绝对寻址,特别是如果您的代码由链接在一起的单独对象组成。在编译时,编译器无法知道对象将停止的位置,也无法知道外部引用的位置或距离有多远,因此通常无法假设它们足够接近以进行基于pc的相对寻址(通常具有范围限制)。因此,编译器通常会为链接器生成一个占位符,以填充绝对地址。解决这个外部地址问题取决于操作和指令集以及其他一些因素。但是,基于项目大小,链接器最终将具有一些绝对寻址。因此,非默认情况通常是生成位置无关代码的命令行选项-PIC,例如您的编译器可能支持。然后编译器和链接器都必须做额外的工作来使这些项成为位置无关的。汇编语言程序员必须自己完成所有这些工作,汇编器通常不会参与其中,它只会为您要生成的指令创建机器码。
novectors.s:
.globl _start
_start:
    b   reset
reset:
    mov sp,#0xD8000000
    bl notmain
    ldr r0,=notmain
    blx r0
hang: b hang

.globl dummy
dummy:
    bx lr

hello.c

extern void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int ra;
    for(ra=0;ra<1000;ra++) dummy(ra);
    return(0);
}

memap (连接脚本) 内存 { ram : ORIGIN = 0xD6000000, LENGTH = 0x4000 } 节 { .text : { (.text) } > ram } Makefile

ARMGNU = arm-none-eabi
COPS = -Wall -O2 -nostdlib -nostartfiles -ffreestanding 
all : hello_world.bin
clean :
    rm -f *.o
    rm -f *.bin
    rm -f *.elf
    rm -f *.list

novectors.o : novectors.s
    $(ARMGNU)-as novectors.s -o novectors.o

hello.o : hello.c
    $(ARMGNU)-gcc $(COPS) -c hello.c -o hello.o

hello_world.bin : memmap novectors.o hello.o 
    $(ARMGNU)-ld novectors.o hello.o -T memmap -o hello_world.elf
    $(ARMGNU)-objdump -D hello_world.elf > hello_world.list
    $(ARMGNU)-objcopy hello_world.elf -O binary hello_world.bin 

hello_world.list(我们关心的部分)


Disassembly of section .text:

d6000000 <_start>:
d6000000:   eaffffff    b   d6000004 <reset>

d6000004 <reset>:
d6000004:   e3a0d336    mov sp, #-671088640 ; 0xd8000000
d6000008:   eb000004    bl  d6000020 <notmain>
d600000c:   e59f0008    ldr r0, [pc, #8]    ; d600001c <dummy+0x4>
d6000010:   e12fff30    blx r0

d6000014 <hang>:
d6000014:   eafffffe    b   d6000014 <hang>

d6000018 <dummy>:
d6000018:   e12fff1e    bx  lr
d600001c:   d6000020    strle   r0, [r0], -r0, lsr #32

d6000020 <notmain>:
d6000020:   e92d4010    push    {r4, lr}
d6000024:   e3a04000    mov r4, #0
d6000028:   e1a00004    mov r0, r4
d600002c:   e2844001    add r4, r4, #1
d6000030:   ebfffff8    bl  d6000018 <dummy>
d6000034:   e3540ffa    cmp r4, #1000   ; 0x3e8
d6000038:   1afffffa    bne d6000028 <notmain+0x8>
d600003c:   e3a00000    mov r0, #0
d6000040:   e8bd4010    pop {r4, lr}
d6000044:   e12fff1e    bx  lr

我展示的是一些位置无关指令和位置相关指令的混合。
例如,这两个指令是一种快捷方式,强制汇编器添加一个类似于.word的内存位置,然后链接器必须填写。
ldr r0,=notmain
blx r0

0xD600001c是该位置。

    d600000c:   e59f0008    ldr r0, [pc, #8]    ; d600001c <dummy+0x4>
    d6000010:   e12fff30    blx r0
...
    d600001c:   d6000020    strle   r0, [r0], -r0, lsr #32

这段代码填充了绝对地址0xD6000020,因此为了使代码正常工作,函数notmain必须位于地址0xD6000020处且不可重定位。但是,该示例的此部分也演示了一些位置无关代码,

ldr r0, [pc, #8]

我所说的PC相对寻址是指该指令集的工作方式是在执行时,PC指向当前PC的两个指令(即PC+8),因此,在内存中如果指令位于0xD600000c,则执行时PC将为0xD6000014,然后根据指令中所述添加8,得到0xD600001C。但是,如果我们将完全相同的机器码指令移动到地址0x1000 并且将其周围的二进制文件(包括它所读取的内容0xD6000020)一起移动。 基本上做到这一点:

    1000:   e59f0008    ldr r0, [pc, #8]    
    1004:   e12fff30    blx r0
...
    1010:   d6000020    

那些指令、机器码仍然有效,不需要重新组装或链接。0xD6000020代码仍然必须在固定地址处,但ldr pc和blx则不需要。

尽管反汇编显示这些基于0xd6...的地址,但bl和bne也是相对于pc的,您可以通过查看指令集文档找到这一点。

d6000030:   ebfffff8    bl  d6000018 <dummy>
d6000034:   e3540ffa    cmp r4, #1000   ; 0x3e8
d6000038:   1afffffa    bne d6000028 <notmain+0x8>

0xD6000030执行时,pc为0xD6000038,0xD6000038-0xD6000018 = 0x20,即8条指令。在二进制补码中,负数8为0xFFF..FFFF8,可以看到ebfffff8的机器码的大部分是ffff8,它被符号扩展并添加到程序计数器中,基本上表示向后跳转8个指令。对于1afffffa中的ffffa,意味着如果不相等,则向后跳转6个指令。请记住,此指令集(arm)假定pc向前两个指令,因此向后6个指令实际上是向前2个指令,然后向后6个指令,或者相当于向后4个指令。
d600000c:   e59f0008    ldr r0, [pc, #8]    ; d600001c <dummy+0x4>
d6000010:   e12fff30    blx r0

然后,整个程序最终成为位置无关的,这是意外的(我恰好知道会发生),但不是因为我告诉工具要这样做,而仅仅是因为我让所有东西都靠近并且没有使用任何绝对寻址。
最后,当你说“无论链接器在哪里找到它们”时,如果你注意到我的链接器脚本,我告诉链接器把所有东西放在0xD6000000开始,我没有指定任何文件名或函数,所以如果没有另有说明,这个链接器将按照命令行上指定的顺序放置项目。hello.c代码是第二个,所以在链接器放置novectors.s代码之后,无论链接器在那里有空间,hello.c代码就在那之后开始,从0xD6000020开始。
而且,一个简单的方法来看哪些是位置无关的,哪些不是,而不必研究每个指令,就是改变链接器脚本以将代码放置在其他地址。
MEMORY
{
    ram : ORIGIN = 0x1000, LENGTH = 0x4000
}
SECTIONS
{
    .text : { *(.text*) } > ram
}

查看机器代码的更改情况,确定哪些发生了变化,哪些没有。

00001000 <_start>:
    1000:   eaffffff    b   1004 <reset>

00001004 <reset>:
    1004:   e3a0d336    mov sp, #-671088640 ; 0xd8000000
    1008:   eb000004    bl  1020 <notmain>
    100c:   e59f0008    ldr r0, [pc, #8]    ; 101c <dummy+0x4>
    1010:   e12fff30    blx r0

00001014 <hang>:
    1014:   eafffffe    b   1014 <hang>

00001018 <dummy>:
    1018:   e12fff1e    bx  lr
    101c:   00001020    andeq   r1, r0, r0, lsr #32

00001020 <notmain>:
    1020:   e92d4010    push    {r4, lr}
    1024:   e3a04000    mov r4, #0
    1028:   e1a00004    mov r0, r4
    102c:   e2844001    add r4, r4, #1
    1030:   ebfffff8    bl  1018 <dummy>
    1034:   e3540ffa    cmp r4, #1000   ; 0x3e8
    1038:   1afffffa    bne 1028 <notmain+0x8>
    103c:   e3a00000    mov r0, #0
    1040:   e8bd4010    pop {r4, lr}
    1044:   e12fff1e    bx  lr

2
你是在暗示位置无关代码和可重定位代码是相同的吗? - PleaseHelp

19

我不确定被接受的答案在这里是否正确。Relocatable Code 和Position-Independent Code之间有根本的差别。

现在,我已经编写汇编代码很长时间,使用过许多不同的架构,并且一直认为机器代码有三种特定的风味:

  • Position-Independent-Code
  • Relocatable-Code
  • Absolute-Code

首先让我们讨论position-independent code。这是一种当汇编后所有指令相对于其他指令的代码。例如,分支指令会从当前指针(或程序计数器,无论您想称之为哪个)指定一个偏移量。位置无关的代码将仅由一个代码段组成,并且其数据也包含在此段(或部分)中。数据嵌入在同一段中存在例外情况,但这些通常是由操作系统或加载器向您传递的好处。

这是一种非常有用的代码类型,因为它意味着操作系统不需要执行任何后加载操作即可开始执行。它将在内存中加载的任何地方运行。当然,这种类型的代码也有其问题,包括不能将可能适合于不同内存类型和限制的代码和数据隔离以及在相关性超出范围之前的大小限制等问题。

Relocatable-Code 在许多方面与 position-independent code 相似,但它有一个非常微妙的区别。正如其名称所示,这种类型的代码是可重定位的,因为代码可以在内存中加载到任何位置,但通常必须在可执行之前进行重新定位或修正。实际上,某些使用这种类型代码的架构针对此目的嵌入了“reloc”部分,以修复代码的可重定位部分。这种类型代码的缺点是,一旦它被重新定位并修正,它几乎变成绝对的,并且固定在其地址上。

所谓可重定位代码的主要优势,也是其成为最常见代码的原因,是它允许将代码轻松分解成部分。每个部分可以在内存中的任何位置加载以适应其要求,然后在重新定位期间,任何引用其他部分的代码都可以使用一个重定位表进行修正,从而将各个部分紧密地连接在一起。代码本身通常是相对的(如x86架构),但不必如此,因为任何可能超出范围的内容都可以被组装为可重定向指令,即由偏移量加上其加载地址构成。这还意味着相对寻址所施加的限制已经不再是问题。
最后一种代码类型是绝对代码。这是编译为在一个特定地址工作的代码,并且只有在加载到该特定地址时才能正常运行。分支和跳转指令都包含一个固定的确切(绝对)地址。这是嵌入式系统中通常发现的一种代码类型,因为可以保证一段代码将被加载到该特定地址,因为只有该代码会被加载至此。在现代计算机上,这样的绝对代码不起作用,因为代码需要加载到任何有空闲内存的地方,无法保证某个内存范围可用。绝对代码确实有其优点,主要是执行速度通常最快,但这可能取决于平台。

7

实际上在代码中包含地址的内容,称作绝对地址。完全使用相对地址的程序(所有操作都使用相对地址完成)可以从任何地址运行。

汇编器并不会这样做,这是由程序员来完成的。我曾经为小程序做过一些操作,通常而言,这是很容易的,但是一旦超出了相对跳转的范围,则变得非常麻烦。如果我没记错的话,唯一的两种方法要么是在例程之间插入相对跳转,要么就是添加已知偏移量到当前地址,将其推入堆栈,然后返回。在早期有第三种计算和写入代码的方法,但现在不再被接受。我已经忘得够久了,所以也不能保证没有其他方法。

如果没有绝对地址,唯一的“调用”方式就是先将想要返回的地址推入堆栈,然后计算地址,将其推入堆栈,最后返回。

需要注意的是,在实际操作中,通常采用混合方案。汇编器和链接器存储了需要进行调整的信息,当程序被加载到内存中时,它会被修改为适应加载的地址。因此,内存中的实际图像是绝对的,但磁盘上的文件看起来像是相对的,但不会引入通常会带来麻烦的问题。(需要注意的是,所有实际生成本机代码的高级语言都使用相同的方法。)


6

基本上,“绝对”模式意味着代码和RAM变量将被放置在你告诉汇编器的确切位置,而“可重定位”模式意味着汇编器构建代码块并指定可以放置在链接器找到空间的RAM需求。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接