GAS ASM PIE x86-64 使用 LEA 指令访问变量

Key*_*Usr 2 x86 assembly gnu-assembler position-independent-code

.data我正在尝试使用 GAS 语法创建一个汇编程序,该程序可以在 x86-64 arch 上以位置无关的方式访问其变量,并强制执行32 位arch 和 IS (%eip而不是%rip)。

无论我尝试什么寄存器,我得到的最好结果都是a Segmentation fault: 11,即使这是为了访问EIP,我根本不应该这样做,因此是SF。最好的结果,因为这至少告诉我一些东西,而不是“嗯,这不行”。

gcc我正在macOS 10.13.6 mid 2010 Intel Core 2 Duo 上编译该文件(这clang可能就是原因):

$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.1.0 (clang-902.0.39.2)
Target: x86_64-apple-darwin17.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
Run Code Online (Sandbox Code Playgroud)

并将一些选项传递给链接器:

gcc -m32 -Wl,-fatal_warnings,-arch_errors_fatal,-warn_commons,-pie test.s
Run Code Online (Sandbox Code Playgroud)

ld:警告:PIE 已禁用。绝对寻址(可能是 -mdynamic-no-pic)在代码签名的 PIE 中不允许,但在 /whatever.../test-a07cf9.o 的 _main 中使用。要修复此警告,请勿使用 -mdynamic-no-pic 进行编译或使用 -Wl,-no_pie ld: fatal warning(s)induced error (-fatal_warnings) clang: error: linker command failed with exit code 1 (use -v 查看调用)1


测试.s

.text
.global _main

_main:
    xor %eax, %eax
    xor %ebx, %ebx

    # lea var1(%esi/edi/ebp/esp), %ebx  # can't compile, not PIE
    # lea var1(%eip), %ebx  # segfault, obvs

    # lea (%esp), %ebx      # EBX = 17
    # lea (%non-esp), %ebx  # segfault

    # lea 0(%esi), %ebx     # segfault 
    # lea 0(%edi), %ebx     # segfault
    # lea 0(%ebp), %ebx     # EBX = 0
    # lea 0(%esp), %ebx     # EBX = 17
    # lea 0(%eip), %ebx     # segfault, obvs

    movl (%ebx), %eax
    ret

.data
    var1: .long 6

.end
Run Code Online (Sandbox Code Playgroud)

我正在运行它来检查最后的./a.out; echo $?EAX 值。ret

我查看了各种来源,但主要是英特尔语法或以下问题之一 - 1 , 2 , 3。我尝试反汇编我能想到的最简单的 C 示例,即来自return-main()的全局变量 + gcc -S test.c -fPIE -pie -fpie -m32

int var1 = 6;
int main() { return var1; }
Run Code Online (Sandbox Code Playgroud)

这基本上导致:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## BB#0:
    pushl   %ebp
Lcfi0:
    .cfi_def_cfa_offset 8
Lcfi1:
    .cfi_offset %ebp, -8
    movl    %esp, %ebp
Lcfi2:
    .cfi_def_cfa_register %ebp
    pushl   %eax
    calll   L0$pb
L0$pb:
    popl    %eax
    movl    $0, -4(%ebp)
    movl    _var1-L0$pb(%eax), %eax
    addl    $4, %esp
    popl    %ebp
    retl
    .cfi_endproc
                                        ## -- End function
    .section    __DATA,__data
    .globl  _var1                   ## @var1
    .p2align    2
_var1:
    .long   6                       ## 0x6


.subsections_via_symbols
Run Code Online (Sandbox Code Playgroud)

显然,这使用 MOV 作为 LEA,并且与我的指令几乎相同,除了-L0$pb应该像 +/- 一样的地址_var1- 地址L0$pb进入该.data部分的部分。

然而,当我尝试使用var1_main标签相同的方法时,什么也没有:

.text
.global _main

_main:
    xor %eax, %eax
    xor %ebx, %ebx

    #movl var1-_main(%ebp), %eax  # EAX = 191
    #movl var1-_main(%esp), %eax  # EAX = 204
    #movl var1-_main(%eax), %eax  # segfault
    ret

.data
    var1: .long 6

.end
Run Code Online (Sandbox Code Playgroud)

有什么想法我做错了什么吗?

编辑:

我设法从反汇编的 C 示例中删除了所有不必要的内容,最终得到以下结果:

.text
.global _main

_main:
    pushl %ebp
    pushl %eax
    calll test

test:
    popl %eax

    /* var1, var2, ... */
    movl var1-test(%eax), %eax

    addl $4, %esp
    popl %ebp
    retl

/**
 * how var1(label) - test(label) skips this label
 * if it's about address subtracting?
 */
blobbbb:
    xor %edx, %edx

.data
var1: .long 6
var2: .long 135
Run Code Online (Sandbox Code Playgroud)

这对我来说没有多大意义,因为根据本指南,调用者应该 1) 将参数压入堆栈(无)2)call标签,而被调用者实际上应该使用 ESP、EBP 和其他寄存器。另外,为什么我需要一个中间标签,或者更好地说,有没有办法不需要它?

fuz*_*fuz 5

在 32 位模式中,没有eip64 位模式中那样的相对寻址模式。因此,代码如下

mov var(%eip), %eax
Run Code Online (Sandbox Code Playgroud)

实际上并不合法,并且不能在 32 位模式下进行汇编。(在 64 位中,它将把地址截断为 32 位)。在传统的非 PIE 32 位二进制文​​件中,您只需这样做

mov var, %eax
Run Code Online (Sandbox Code Playgroud)

它将绝对地址处的值移动到vareax但这在 PIE 二进制文件中是不可能的,因为 的绝对地址var在链接时未知。

链接器所知道的是二进制文件的布局以及标签之间的距离是多少。因此,要访问全局变量,您可以像这样进行:

  1. 找出某个标签的绝对地址并用它加载某个寄存器
  2. 添加到该标签的距离var
  3. 访问变量

步骤 2 和步骤 3 可以使用带有位移的寻址模式组合起来。第 1 步很棘手。只有一条有用的指令可以告诉我们不知道地址的位置的地址是什么,那就是call:该call指令将下一条指令的地址压入堆栈,然后跳转到指示的地址。如果我们告诉call只跳转到下一个地址,我们就会将其功能减少到本质上push %eip

        call Label                  # like push %eip
Label:  ...
Run Code Online (Sandbox Code Playgroud)

请注意,此用例在 CPU 的返回预测中是特殊情况,实际上并不算作函数调用。由于这不是真正的函数调用,因此我们没有建立堆栈帧或类似的框架,并且我们没有此调用的返回。它只是一种获取指令指针值的机制。

由此,我们知道了 的地址Label。接下来我们可以将其从堆栈中弹出并使用它来查找 的地址var

        call Label
Label:  pop %eax                    # eax = Label
        add $var-Label, %eax        # eax = Label + var - Label = var
Run Code Online (Sandbox Code Playgroud)

然后我们可以取消引用它来获取以下内容var

        call Label
Label:  pop %eax
        add %eax, $var-Label
        mov (%eax), %eax            # eax = *var
Run Code Online (Sandbox Code Playgroud)

在实际代码中,您可以合并加法和内存操作数以保存指令:

        call Label
Label:  pop %eax
        mov var-Label(%eax), %eax   # eax = *var
Run Code Online (Sandbox Code Playgroud)

如果你想在一个函数中引用多个静态变量,你只需要使用这个技巧一次。只需使用适当的差异:

        call Label
Label:  pop %eax
        mov foo-Label(%eax), %ebx   # ebx = *foo
        mov bar-Label(%eax), %ecx   # ecx = *bar
Run Code Online (Sandbox Code Playgroud)

请注意,gcc 支持此习惯用法的变体来获取指令指针的内容。它创建了一堆这样的函数:

___x86.get_pc_thunk.bx:
        mov (%esp), %ebx
        ret
Run Code Online (Sandbox Code Playgroud)

将返回地址移动到指定的寄存器。这是一个特殊函数,不遵循正常的调用约定eaxebxecx、 、edxesi和中的每一个都存在一个edi函数,具体取决于 gcc 要使用的寄存器。代码如下所示:

        call ___x86.get_pc_thunk.bx # ebx = Label
Label:  mov foo-Label(%ebx), %eax   # eax = *foo
        mov bar-Label(%ebx), %ecx   # ecx = *bar
Run Code Online (Sandbox Code Playgroud)

gcc 使用此代码在返回预测不考虑此假调用习惯用法的 CPU 上获得更好的性能。但我不知道哪些 CPU 实际上受到了影响。

最后请注意,没有跳过任何标签。我不太明白你的意思blobbbb。哪个控件应该达到这个标签?

最后,您的示例应如下所示:

        .text
        .global _main

_main:  call Label                  # push %eip
Label:  pop %eax                    # eax = Label
        mov var1-Label(%eax), %eax  # eax = *(Label+var1-Label)
        ret


        .data
var1:   .long 6
Run Code Online (Sandbox Code Playgroud)

.end请注意,永远不需要该指令。以大写字母开头的标签L是局部标签,不会出现在符号表中,这就是 C 编译器喜欢使用它们的原因。

  • 回复:thunks:人们普遍认为 `call +0` / `pop` 会使大多数 CPU 上的返回地址预测器不平衡。IIRC,Agner Fog 的导游甚至提出了这样的说法。但http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0实际测量发现,只有Via Nano不支持特殊情况的call/pop。可能有足够多的现有代码使用了 call/pop 习惯用法,因此值得 CPU 供应商对其进行特殊处理。调用 thunk 比内联调用/pop 短 1 个字节,因此在大型库中,它可能会节省一些总代码大小。 (4认同)