我的问题假设程序是使用linux托管环境编译的,而不是独立的(即嵌入式系统)。我对这个问题的兴趣是学术上的一半,但我确实在我的编程环境中使用了Ubuntu Linux 12.04.04 LTS,我只想知道它是如何工作的。
为了理解我的问题,我写了一些导入。如果我们同意以下内容是伪代码,那么任何错误都可能被掩盖,而答案可以围绕原则而不是具体实现。
int main(void)
{
int j;
j = 2;
if (j == 2) {
j = j + 1;
} else {
j = j - 1;
}
return 0;
}
汇编输出将具有比较和分支指令,例如在下面的伪汇编代码中。这是我的假设,所以如果我的理解不顺利,请让我走上正轨......
.start
LOAD J, #2 ; j = 2
CMP J, #2 ; if (j == 2)
BNE .else
INC J, #1 ; j = j + 1
.else
DEC J, #1 j j = j - 1
因此,生成的机器代码可能导致分支相对于PC跳转。如果我们同意每个指令总是32位宽,为了简单起见,那么“BNE .else”可能会产生一个机器代码指令,它增加PC = PC + 8,因为那是相对于分支的位置,(基本上跳过“INC J,#1”指令。
但是,如果.else块更远,那么“BNE .else”可能会导致绝对跳转到内存中的特定位置,在这种情况下,地址将是.start + 16个字节。
如果相对跳转示例始终为真,那么程序可以复制到内存的任何部分,比如
0x20000000或
0x30000000然后PC移动到该位置并且代码开始运行。在每种情况下,分支的操作码分别为
BNE 0x20000010和
BNE 0x30000010(假设我们找到了一种方法,通过对齐4或8字节边界将BNE操作码压缩到地址中) 。
如果第二个绝对跳转示例为真,则需要在编译时知道起始位置,否则操作系统必须在运行程序之前重新定位分支指令。
如果编译器假定代码将在
0x00000000运行,但操作系统希望代码在
0x20000000运行,则操作系统需要在加载时更改分支语句将代码放入内存,放下新的跳转地址。
所以我的问题是:
通常,编译器是生成相对或绝对机器代码,还是两者都生成?也许它永远不会产生绝对的样式代码来避免所有这些......
如果它确实生成了绝对的机器代码,它会假设代码将开始到内存中的哪个地址?
如果它没有修复内存中的地址,那么它是否将机器码编写为好像它将从零地址开始,操作系统在运行之前如何取代分支指令?
答案 0 :(得分:3)
我认为这些文章将有助于回答您的问题:
简短的回答是编译器生成以可重定位格式打包的绝对机器代码。启动可重定位的可执行文件时,地址是固定的。
大多数现代硬件都有一个内存管理单元,可以大大简化此过程,因为该过程在虚拟地址空间中执行,无需重新定位地址。
共享库通常使用-f PIC(或等效)选项进行编译,以生成与位置无关的代码。
答案 1 :(得分:3)
首先,您应该知道编译器会创建目标文件,链接器会将多个目标文件链接到可执行文件中。
目标文件仅包含地址的“占位符”(例如“jmp 0x0”),它将被链接器替换(例如将“jmp 0x0”替换为“jmp 0x12345678”)。
可以使用某些编译器命令行选项编译与位置无关的内容。这用于共享库,因为这些代码必须由于某些原因在内存中的不同位置运行而不进行更改。默认情况下,编译器使用绝对寻址(链接器将插入地址)。
可执行文件格式包含有关加载可执行文件的地址的信息。与Windows(可执行文件可能在某些情况下移动到另一个位置 - 但不是所有情况)不同,加载可执行文件的地址是固定的并由链接器提供。共享库可以加载到内存中的任何地址。因此,他们必须使用相对寻址而不是绝对寻址。
如果您想知道不同的程序如何“共享”相同的地址,请阅读Brad Lanam的答案中给出的内存管理单元的链接。
在没有MMU的机器上(例如20世纪80年代后期的Amiga模型的Linux版本 - 是的,它们可以运行Linux!)可执行文件也必须与位置无关,因为它们无法加载到固定的地址。
顺便说一句:几乎所有CPU上的跳转指令(x86,ARM,MIPS,SPARC,PowerPC)都是相对的,而不是绝对的。 (值得注意的例外是x86上的“远程跳转”和MIPS上的“J(AL)”指令。