gdb如何拆解工作?

时间:2018-01-21 11:01:33

标签: c gcc assembly gdb disassembly

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

例如,执行以下命令后,

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

转储代码为:

dump code

当我退出gdb并重新组装main函数时,我将得到与之前相同的结果。对于gdb中的每个反汇编命令,指令地址甚至常量地址总是相同的。这是为什么?编译文件hello是否包含有关每个汇编指令的地址的某些信息以及常量'地址?

4 个答案:

答案 0 :(得分:2)

如果您创建了与位置无关的可执行文件(例如,使用gcc -fpie -pie, which is the default for gcc in many recent Linux distros),则内核会将其映射到您的可执行文件的地址随机化。 (除非在GDB下运行:GDB disables ASLR by default,即使对于共享库和PIE可执行文件也是如此。)

但是您正在制作一个位置依赖可执行文件,它可以利用静态地址作为链接时间常量(通过将它们用作immediates等等而无需运行时重定位修复) 。例如您或编译器可以使用mov $msg, %edi(与您的代码一样)而不是lea msg, %rdi(使用-fpie)。

常规(位置相关)可执行文件在ELF标头中设置了加载地址:使用readelf -a ./a.out查看ELF元数据。

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

e.g。在一个简单的可执行文件中(只有一个文本段,而不是数据或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 

因此,ELF标头请求将文件中的偏移量0映射到虚拟地址0x0000000000400000。 (并且ELF入口点为0x400080; _start所在的位置。)我不确定PhysAddr = VirtAddr的相关性是什么;用户空间可执行文件不知道,并且不能轻易找出内核用于支持其虚拟内存的RAM页面的内容,并且随着页面交换进出,它可以随时更改。 / p>

请注意,readelf会换行;请注意,有两行列标题。 0x200000是该一个LOADed细分的Align列。

答案 1 :(得分:1)

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

但是,即使对于与位置无关的可执行文件(PIE),GDB运行之间的地址也会保持不变,因为默认情况下为GDB disables address space layout randomization。 GDB执行此操作,以便在程序启动后可以重新应用绝对地址的断点。

答案 2 :(得分:0)

有各种可执行文件格式。通常,可执行文件包含多个内存部分部分的信息。在可执行文件内部,可以相对于段的开头表达对存储器地址的引用。可执行文件还包含重定位表。重定位表是这些引用的列表,包括每个引用在可执行文件中的位置,它引用的部分,引用的引用类型(使用它的指令的哪个字段,等。)。

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

好的现代装载程序随机化部分加载的位置。他们这样做是因为恶意人员有时能够利用程序中的错误导致他们执行攻击者注入的代码。随机化部分位置可防止攻击者知道其代码将被注入的地址,这可能会妨碍他们准备要注入的代码的能力。由于您的地址没有变化,您的加载程序似乎不会这样做。您可能正在使用较旧的系统。

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

答案 3 :(得分:0)

不打算成为真正执行的程序

自举

.globl _start
_start:
    bl one
    b .

第一个c文件

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

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

unsigned int hello;

链接描述文件

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

依赖建筑位置

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

链接器在链接时填充此内容。您可以在上面的反汇编中看到该位置依赖于它填写查找该项目的位置的绝对地址。要使此代码起作用,必须以该项显示在该地址的方式加载代码。它必须加载到内存中的特定位置或地址。位置依赖。 (基本上加载到地址0x1000)。

如果您的工具链支持位置无关(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

当然它有性能影响,但是现在有一个表,全局偏移表(对于这个解决方案),而不是编译器和链接器通过离开一个位置一起工作,它位于相对于代码,包含链接器提供的偏移量。

程序不是位置独立的,如果你把它加载到任何地方肯定不会起作用。加载器必须根据它想要放置项目的位置来修补表/解决方案。这比在第一个解决方案中修补每个位置的很长列表要简单得多,尽管这可能是一种非常可行的方法。可执行文件中的一个表(可执行文件包含的程序和数据超过了你知道的其他信息项,如果你对elf文件进行objdump或readelf)可以包含所有这些偏移量,加载程序也可以修补它们。

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

如果内置位置无关,则说明内核将或将始终随机化您的位置是不正确的。只要工具链和来自操作系统的装载机(一个完全独立的开发)携手合作,装载机就有机会。但这并不意味着每个装载机都会或将会。特定的操作系统/发行版/版本可能会将其设置为默认值。如果他们遇到一个与位置无关的二进制文件(以加载器期望的方式构建)。这就像说,如果你带着特定品牌的汽车出现在他们的车库里,那么这个星球上的所有机械师都将使用特定的品牌和类型的油。特定的机械师可能总是使用特定的油品牌和特定汽车的类型,但这并不意味着所有机械师将或甚至可以获得特定的石油品牌或类型。如果该个体企业选择作为一项政策,那么作为一个客户,您可以开始形成一个假设,即您将得到的结果(随着该假设,当他们改变其政策时失败)。

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

然后是调试器的一部分,为了让调试器工作/显示正确的信息,它也必须是工具链/加载器(/ os)团队的一部分,以便工作/看起来正确。它必须知道这是位置无关的,并且必须知道它的加载位置和/或调试器正在为您进行加载,并且可能不会像命令行或gui那样使用标准OS加载程序。因此,每次使用调试器时,您仍可能在同一位置看到二进制文件。

这里的主要错误是你的期望。 Windows,Linux等第一个操作系统希望使用MMU来更好地管理内存。选择一些/许多非线性物理内存块并为程序创建一个虚拟内存的线性区域,更重要的是每个单独程序的虚拟地址空间看起来都一样,我可以让每个程序加载到0x8000虚拟地址空间,不会相互干扰,为此设计的MMU和利用此功能的操作系统。即使使用这种MMU和操作系统以及位置无关的加载,人们也希望它们不使用物理地址,它们仍然在创建虚拟地址空间,可能只为每个程序或程序的每个实例提供不同的加载点。期望所有操作系统始终如一地执行此操作是期望问题。当使用调试器时,您不在库存环境中,程序运行方式不同,可以加载不同等等。这与没有调试器的情况下运行不同,因此使用调试器也会改变您应该看到的情况。这里有两个层次的期望来处理。

如上所述,在一个非常简单的程序中使用外部组件,参见它为了位置独立而构建的对象的反汇编,以及在链接中然后尝试Linux,就像Peter指示的那样,看看它是否加载每次都有一个不同的地方,如果不是那么你需要看超级用户SE或google周围关于如何使用linux(和/或gdb)来改变加载位置。