为什么每次在 GDB 中构建和反汇编一个函数时我都得到相同的地址?

Why do I get the same address every time I build + disassemble a function inside GDB?

为什么我每次反汇编函数时,总是得到相同的指令地址和常量地址?

例如执行以下命令后,

gcc -o hello hello.c -ggdb
gdb hello
(gdb) disassemble main

转储代码为:

当我退出 gdb 和 re-disassemble main 函数时,我会得到与以前相同的结果。对于 gdb 中的每个反汇编命令,指令地址甚至常量地址始终相同。这是为什么?编译后的文件hello是否包含有关每个汇编指令的地址以及常量地址的特定信息?

executable 文件格式多种多样。通常,一个 executable 文件包含一些内存 部分 的信息。在 executable 中,对内存地址的引用可以相对于节的开头来表示。 executable 还包含一个 重定位 table。重定位 table 是这些引用的列表,包括每个引用在 executable 中的位置、它引用的部分以及它是什么类型的引用(它使用了指令的哪个字段在,等等).

加载程序(将程序加载到内存中的软件)读取 executable 并将这些部分写入内存。在您的情况下,加载程序似乎在每次运行时都对部分使用相同的基地址。在最初将这些部分放入内存后,加载程序读取重定位 table 并使用它来修复所有对内存的引用,方法是根据每个部分加载到内存中的位置调整它们。例如,编译器可能会编写一条指令,实际上是“从数据段的开头加载寄存器 3 加上 278 个字节”。如果加载程序将数据部分放在地址 2000,它将调整此指令以使用 2000 和 278 的和,使得“从地址 2278 加载寄存器 3”。

优秀的现代加载程序会随机化加载部分的位置。他们这样做是因为怀有恶意的人有时能够利用程序中的错误使他们执行攻击者注入的代码。随机化部分位置可以防止攻击者知道他们的代码将被注入的地址,这会阻碍他们准备要注入的代码的能力。由于您的地址没有改变,因此您的加载程序似乎没有这样做。您可能使用的是旧系统。

一些处理器架构 and/or 加载程序支持 位置无关代码 (PIC)。在这种情况下,指令的形式可能是“从超出该指令所在位置的 694 字节加载寄存器 3”。在那种情况下,只要数据始终与指令保持相同的距离,它们在内存中的位置就无关紧要了。当进程执行指令时,它将指令的地址加上694,这将是数据的地址。实现类 PIC 代码的另一种方法是让加载程序通过将这些地址放入寄存器或内存中的固定位置来为程序提供每个部分的地址。然后程序可以使用这些基地址进行自己的地址计算。由于您的程序在代码中内置了一个地址,因此您的程序似乎没有使用这些方法。

如果您制作了 position-independent 可执行文件(例如 gcc -fpie -pie, which is the default for gcc in many recent Linux distros), the kernel would randomize the address it mapped your executable at. (Except when running under GDB: GDB disables ASLR by default 甚至用于共享库和 PIE 可执行文件。)


但是你正在制作一个 position-dependent 可执行文件,它可以利用静态地址作为 link-time 常量(通过将它们用作立即数等而不需要运行时重定位修复)。例如您或编译器可以使用 mov $msg, %edi (如您的代码)而不是 lea msg, %rdi (使用 -fpie)。

常规 (position-dependent) 可执行文件在 ELF load-address 中设置 headers:使用 readelf -a ./a.out 查看 ELF 元数据。

一个non-PIE可执行文件每次都会同时加载,即使在GDB下没有运行它,在ELF程序headers中指定的地址。 (gcc / ld 在 x86-64-linux-elf 上默认选择 0x400000;您可以使用链接描述文件更改它)。代码+数据中所有静态地址的重定位信息hard-coded不可用,因此加载器无法修复地址,即使它想修复。

例如在一个简单的可执行文件中(只有一个文本段,没有数据或 bss)我用 -no-pie 构建(这似乎是你的 gcc 中的默认值):

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000000c5 0x00000000000000c5  R E    0x200000

 Section to Segment mapping:
  Segment Sections...
   00     .text 

所以ELFheaders请求将文件中的偏移量0映射到虚拟地址0x0000000000400000。 (ELF 入口点是 0x400080;那是 _start 所在的位置。)我不确定 PhysAddr = VirtAddr 的相关性是什么; user-space 可执行文件不知道也不能轻易找出内核用于支持其虚拟内存的 RAM 页面的物理地址,并且随着页面换入/换出,它可以随时更改。

请注意 readelf 会自动换行;注意有两行列 headers。 0x200000 是该加载段的对齐列。

默认情况下,x86-64 的 GNU 工具链 Linux 生成映射到地址 0x400000 的位置相关可执行文件。 (与位置无关的可执行文件将映射到 0x55… 地址)。可以通过构建 GCC --enable-default-pie 或通过指定编译器和链接器标志来更改它。

然而,即使对于位置无关的可执行文件 (PIE),地址在 GDB 运行之间也是不变的,因为默认情况下 GDB disables address space layout randomization。 GDB 这样做是为了在程序启动后可以重新应用绝对地址处的断点。

一个不打算真正执行的程序

bootstrap

.globl _start
_start:
    bl one
    b .

第一个c文件

extern unsigned int hello;
unsigned int one ( void )
{
    return(hello+5);
}

第二个c文件(extern强制编译器以某种方式编译第一个对象)

unsigned int hello;

link脚本

MEMORY
{
    ram : ORIGIN = 0x00001000, LENGTH = 0x4000
}
SECTIONS
{
    .text : { *(.text*) } > ram
    .bss : { *(.bss*) } > ram
}

构建 pos依赖于环境

Disassembly of section .text:

00001000 <_start>:
    1000:   eb000000    bl  1008 <one>
    1004:   eafffffe    b   1004 <_start+0x4>

00001008 <one>:
    1008:   e59f3008    ldr r3, [pc, #8]    ; 1018 <one+0x10>
    100c:   e5930000    ldr r0, [r3]
    1010:   e2800005    add r0, r0, #5
    1014:   e12fff1e    bx  lr
    1018:   0000101c    andeq   r1, r0, r12, lsl r0

Disassembly of section .bss:

0000101c <hello>:
    101c:   00000000    andeq   r0, r0, r0

这里的关键是地址 0x1018,编译器必须为外部项的地址保留一个占位符。显示为下面的偏移量 0x10

00000000 <one>:
   0:   e59f3008    ldr r3, [pc, #8]    ; 10 <one+0x10>
   4:   e5930000    ldr r0, [r3]
   8:   e2800005    add r0, r0, #5
   c:   e12fff1e    bx  lr
  10:   00000000    andeq   r0, r0, r0

link 人在 link 时填写。您可以在上面的反汇编中看到依赖于 pos 的它填充了找到该项目的绝对地址。要使此代码正常工作,必须以该项目显示在该地址的方式加载代码。它必须加载到特定的 pos 位置或内存中的地址。 Pos状态依赖。 (基本上加载到地址 0x1000)。

如果您的工具链支持 pos 独立(gnu 支持)那么这就是一个解决方案。

Disassembly of section .text:

00001000 <_start>:
    1000:   eb000000    bl  1008 <one>
    1004:   eafffffe    b   1004 <_start+0x4>

00001008 <one>:
    1008:   e59f3014    ldr r3, [pc, #20]   ; 1024 <one+0x1c>
    100c:   e59f2014    ldr r2, [pc, #20]   ; 1028 <one+0x20>
    1010:   e08f3003    add r3, pc, r3
    1014:   e7933002    ldr r3, [r3, r2]
    1018:   e5930000    ldr r0, [r3]
    101c:   e2800005    add r0, r0, #5
    1020:   e12fff1e    bx  lr
    1024:   00000014    andeq   r0, r0, r4, lsl r0
    1028:   00000000    andeq   r0, r0, r0

Disassembly of section .got:

0000102c <.got>:
    102c:   0000103c    andeq   r1, r0, r12, lsr r0

Disassembly of section .got.plt:

00001030 <_GLOBAL_OFFSET_TABLE_>:
    ...

Disassembly of section .bss:

0000103c <hello>:
    103c:   00000000    andeq   r0, r0, r0

它当然会影响性能,但不是编译器和 linker 通过留下一个位置一起工作,现在有一个 table,全局偏移 table(对于此解决方案)位于已知位置,该位置相对于代码 pos ,包含 linker 提供的偏移量。

该程序还不是pos独立的,如果您在任何地方加载它肯定无法运行。加载程序必须根据要放置项目的位置修补 table/solution。这比在第一个解决方案中列出每个要修补的位置的长列表要简单得多,尽管这样做是一种非常 pos 可行的方法。 executable 中的一个 table(executable 包含的不仅仅是程序和数据,它们还包含其他信息项,如果您对 elf 文件进行 objdump 或 readelf,您就会知道)可能包含所有ose 的偏移量和加载程序也可以修补 those。

如果您的数据和 bss 以及其他内存部分相对于我在此处构建的 .text 是固定的,那么就没有必要 linker 可以在 link 时间计算相对资源的偏移量以及编译器以 pos 独立的方式找到了该项目,并且二进制文件几乎可以加载到任何地方(可能需要一些最小对齐)并且它可以在没有任何补丁的情况下工作.使用 gnu 解决方案,我认为您可以相对于彼此移动段。

如果构建独立于 pos ,内核将或将始终随机化您的位置的说法是不正确的。虽然 possible 只要工具链和来自操作系统的加载程序(完全独立的开发)携手合作,加载程序就有机会。但这绝不意味着每个装载机都会或将要这样做。具体操作systems/distros/versions可能已经设置为默认yes了。如果他们来 across 一个独立于 position 的二进制文件(以加载程序期望的方式构建)。这就像说如果你开着特定品牌的汽车出现在他们的车库里,地球上所有的机械师都会使用特定品牌和类型的机油。特定的机械师可能总是为特定的汽车使用特定的机油品牌和类型,但这并不意味着所有的机械师都会或什至可以获得该特定的机油品牌或类型。如果该个别企业选择os作为一项政策,那么作为客户的您可以开始假设这就是您将得到的(假设在他们改变政策时失败)。

就反汇编而言,您可以在构建时或任何时候静态反汇编您的项目。如果在不同的 pos 位置加载,那么您所看到的将会有一个偏移量,但是 .text 代码相对于该段中的其他代码仍将位于同一位置。如果静态反汇编显示调用提前 0x104 字节,那么即使加载到其他地方,您也应该看到相对跳转也提前 0x104 字节,地址可能不同。

然后是调试器部分,为了调试器 work/show 正确的信息,它还必须是 toolchain/loader(/os) 团队的一部分work/look 对。它必须知道这是独立于 pos 的,并且必须知道它被加载到哪里 and/or 调试器正在为你加载并且可能不会使用标准的 OS 加载器命令行或图形用户界面的方式。因此,每次使用调试器时,您可能仍会在同一位置看到二进制文件。

这里的主要错误是您的期望。 windows、linux 等第一个操作系统希望使用 MMU 来更好地管理内存。选择 some/many 非线性物理内存块并为您的程序创建线性虚拟内存区域,更重要的是每个单独程序的虚拟地址 space 看起来都一样,我可以每个程序都加载到虚拟地址 space 中的 0x8000,而不会相互干扰,具有为此设计的 MMU 和利用此优势的操作系统。即使有了这个 MMU 和操作系统以及 position 独立加载,人们希望他们不使用物理地址,他们仍然在创建一个虚拟地址 space,只是 possibly 不同每个程序或程序的每个实例的加载点。期望所有操作系统一直这样做是一个期望问题。当使用调试器时,您不在库存环境中,程序运行不同,可以不同地加载等等。它与没有调试器的 运行 不同,因此使用调试器也会改变您应该期望的看到发生。这里需要处理两个级别的期望。

在我上面制作的非常简单的程序中使用外部组件,请参阅它为 pos 独立性构建的对象的反汇编以及 linking然后按照 Peter 的指示尝试 Linux,看看它是否每次都加载到不同的地方,如果不是,那么您需要查看超级用户 SE 或 google 了解如何使用 linux (and/or gdb) 让它改变加载位置。