GAS ASM PIE x86-64 使用 LEA 指令访问变量
GAS ASM PIE x86-64 access variable with LEA instruction
我正在尝试创建一个具有 GAS 语法的汇编程序,它可以在 x86-64 arch 上以位置独立的方式从 .data
部分访问它的变量,并强制执行 32bit arch 和 IS(%eip
而不是 %rip
)。
无论我尝试过什么寄存器,我得到的最好结果是 Segmentation fault: 11
甚至那是为了访问我根本不应该做的 EIP,因此是 SF。最好的结果,因为它至少告诉了我除 "meh, it won't do".
之外的其他信息
我在 macOS 10.13.6 mid 2010 Intel Core 2 Duo 上用 gcc
编译文件(这就是 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
并将一些选项传递给链接器:
gcc -m32 -Wl,-fatal_warnings,-arch_errors_fatal,-warn_commons,-pie test.s
ld: warning: PIE disabled. Absolute addressing (perhaps -mdynamic-no-pic) not allowed in code signed PIE, but used in _main from /whatever.../test-a07cf9.o. To fix this warning, don't compile with -mdynamic-no-pic or link with -Wl,-no_pie
ld: fatal warning(s) induced error (-fatal_warnings)
clang: error: linker command failed with exit code 1 (use -v to see invocation)
1
test.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
我运行用./a.out; echo $?
检查最后来自ret
的EAX值。
我查看了各种来源,但主要是英特尔语法或这些问题之一 - 1, 2, 3。我试图反汇编我能想到的最简单的 C 示例,即全局变量 + return
来自 main()
- gcc -S test.c -fPIE -pie -fpie -m32
:
int var1 = 6;
int main() { return var1; }
这基本上导致:
.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 [=14=], -4(%ebp)
movl _var1-L0$pb(%eax), %eax
addl , %esp
popl %ebp
retl
.cfi_endproc
## -- End function
.section __DATA,__data
.globl _var1 ## @var1
.p2align 2
_var1:
.long 6 ## 0x6
.subsections_via_symbols
这显然使用 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
知道我做错了什么吗?
编辑:
我设法从反汇编的 C 示例中删除了任何不必要的东西,结果是这样的:
.text
.global _main
_main:
pushl %ebp
pushl %eax
calll test
test:
popl %eax
/* var1, var2, ... */
movl var1-test(%eax), %eax
addl , %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
这对我来说没有多大意义,因为根据 this guide 调用者应该 1) 将参数压入堆栈 (none) 2) call
标签和 callee 实际上应该与 ESP、EBP 和其他寄存器一起使用。另外,为什么我什至需要一个中间标签或者更好地说,没有它有什么办法吗?
在 32 位模式下,没有 64 位模式下的 eip
相对寻址模式。因此,像
这样的代码
mov var(%eip), %eax
实际上不合法,在 32 位模式下 assemble 也不合法。 (在 64 位中,它将地址截断为 32 位)。在传统的非 PIE 32 位二进制文件中,您只需执行
mov var, %eax
将 var
的绝对地址的值移动到 eax
,但这在 PIE 二进制文件中是不可能的,因为 var
的绝对地址在 [=83= 是未知的] 时间。
linker 知道的是二进制文件的布局以及标签之间的距离。因此,要访问全局变量,您可以这样进行:
- 找出一些标签的绝对地址并用它加载一些寄存器
- 加上从该标签到
var
的距离
- 访问变量
步骤 2 和 3 可以结合使用带位移的寻址模式。第 1 步很棘手。只有一条有用的指令告诉我们不知道其地址的位置的地址是什么,那就是 call
:call
指令将下一条指令的地址压入堆栈,然后跳转到指定地址。如果我们告诉 call
只是跳转到下一个地址,我们将其功能减少到本质上是 push %eip
:
call Label # like push %eip
Label: ...
请注意,此用例在 CPU 的 return 预测中是特殊情况,实际上不算作函数调用。由于这不是真正的函数调用,因此我们没有建立堆栈框架或类似结构,也没有用于此调用的 return。它只是一种获取指令指针值的机制。
由此,我们知道Label
的地址。接下来我们可以将其弹出堆栈并使用它来查找 var
:
的地址
call Label
Label: pop %eax # eax = Label
add $var-Label, %eax # eax = Label + var - Label = var
然后我们可以取消引用它以获取 var
:
的内容
call Label
Label: pop %eax
add %eax, $var-Label
mov (%eax), %eax # eax = *var
在实际代码中,您将合并加法和内存操作数以保存一条指令:
call Label
Label: pop %eax
mov var-Label(%eax), %eax # eax = *var
如果你想在一个函数中引用多个静态变量,你只需要使用一次这个技巧。只需使用 suitable 差异:
call Label
Label: pop %eax
mov foo-Label(%eax), %ebx # ebx = *foo
mov bar-Label(%eax), %ecx # ecx = *bar
请注意,gcc 支持使用此习语的变体来获取指令指针的内容。它创建了一堆这样的函数:
___x86.get_pc_thunk.bx:
mov (%esp), %ebx
ret
将 return 地址移动到指定的寄存器。这是一个不遵循正常调用约定的特殊函数,eax
、ebx
、ecx
、edx
、esi
和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
gcc 使用此代码在 CPU 上获得更好的性能,其 return 预测未考虑此假呼叫成语。不过,我不知道哪些 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
请注意,永远不需要 .end
指令。以大写 L
开头的标签是不以符号 table 结尾的局部标签,这就是 C 编译器喜欢使用它们的原因。
我正在尝试创建一个具有 GAS 语法的汇编程序,它可以在 x86-64 arch 上以位置独立的方式从 .data
部分访问它的变量,并强制执行 32bit arch 和 IS(%eip
而不是 %rip
)。
无论我尝试过什么寄存器,我得到的最好结果是 Segmentation fault: 11
甚至那是为了访问我根本不应该做的 EIP,因此是 SF。最好的结果,因为它至少告诉了我除 "meh, it won't do".
我在 macOS 10.13.6 mid 2010 Intel Core 2 Duo 上用 gcc
编译文件(这就是 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
并将一些选项传递给链接器:
gcc -m32 -Wl,-fatal_warnings,-arch_errors_fatal,-warn_commons,-pie test.s
ld: warning: PIE disabled. Absolute addressing (perhaps -mdynamic-no-pic) not allowed in code signed PIE, but used in _main from /whatever.../test-a07cf9.o. To fix this warning, don't compile with -mdynamic-no-pic or link with -Wl,-no_pie ld: fatal warning(s) induced error (-fatal_warnings) clang: error: linker command failed with exit code 1 (use -v to see invocation) 1
test.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
我运行用./a.out; echo $?
检查最后来自ret
的EAX值。
我查看了各种来源,但主要是英特尔语法或这些问题之一 - 1, 2, 3。我试图反汇编我能想到的最简单的 C 示例,即全局变量 + return
来自 main()
- gcc -S test.c -fPIE -pie -fpie -m32
:
int var1 = 6;
int main() { return var1; }
这基本上导致:
.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 [=14=], -4(%ebp)
movl _var1-L0$pb(%eax), %eax
addl , %esp
popl %ebp
retl
.cfi_endproc
## -- End function
.section __DATA,__data
.globl _var1 ## @var1
.p2align 2
_var1:
.long 6 ## 0x6
.subsections_via_symbols
这显然使用 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
知道我做错了什么吗?
编辑:
我设法从反汇编的 C 示例中删除了任何不必要的东西,结果是这样的:
.text
.global _main
_main:
pushl %ebp
pushl %eax
calll test
test:
popl %eax
/* var1, var2, ... */
movl var1-test(%eax), %eax
addl , %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
这对我来说没有多大意义,因为根据 this guide 调用者应该 1) 将参数压入堆栈 (none) 2) call
标签和 callee 实际上应该与 ESP、EBP 和其他寄存器一起使用。另外,为什么我什至需要一个中间标签或者更好地说,没有它有什么办法吗?
在 32 位模式下,没有 64 位模式下的 eip
相对寻址模式。因此,像
mov var(%eip), %eax
实际上不合法,在 32 位模式下 assemble 也不合法。 (在 64 位中,它将地址截断为 32 位)。在传统的非 PIE 32 位二进制文件中,您只需执行
mov var, %eax
将 var
的绝对地址的值移动到 eax
,但这在 PIE 二进制文件中是不可能的,因为 var
的绝对地址在 [=83= 是未知的] 时间。
linker 知道的是二进制文件的布局以及标签之间的距离。因此,要访问全局变量,您可以这样进行:
- 找出一些标签的绝对地址并用它加载一些寄存器
- 加上从该标签到
var
的距离
- 访问变量
步骤 2 和 3 可以结合使用带位移的寻址模式。第 1 步很棘手。只有一条有用的指令告诉我们不知道其地址的位置的地址是什么,那就是 call
:call
指令将下一条指令的地址压入堆栈,然后跳转到指定地址。如果我们告诉 call
只是跳转到下一个地址,我们将其功能减少到本质上是 push %eip
:
call Label # like push %eip
Label: ...
请注意,此用例在 CPU 的 return 预测中是特殊情况,实际上不算作函数调用。由于这不是真正的函数调用,因此我们没有建立堆栈框架或类似结构,也没有用于此调用的 return。它只是一种获取指令指针值的机制。
由此,我们知道Label
的地址。接下来我们可以将其弹出堆栈并使用它来查找 var
:
call Label
Label: pop %eax # eax = Label
add $var-Label, %eax # eax = Label + var - Label = var
然后我们可以取消引用它以获取 var
:
call Label
Label: pop %eax
add %eax, $var-Label
mov (%eax), %eax # eax = *var
在实际代码中,您将合并加法和内存操作数以保存一条指令:
call Label
Label: pop %eax
mov var-Label(%eax), %eax # eax = *var
如果你想在一个函数中引用多个静态变量,你只需要使用一次这个技巧。只需使用 suitable 差异:
call Label
Label: pop %eax
mov foo-Label(%eax), %ebx # ebx = *foo
mov bar-Label(%eax), %ecx # ecx = *bar
请注意,gcc 支持使用此习语的变体来获取指令指针的内容。它创建了一堆这样的函数:
___x86.get_pc_thunk.bx:
mov (%esp), %ebx
ret
将 return 地址移动到指定的寄存器。这是一个不遵循正常调用约定的特殊函数,eax
、ebx
、ecx
、edx
、esi
和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
gcc 使用此代码在 CPU 上获得更好的性能,其 return 预测未考虑此假呼叫成语。不过,我不知道哪些 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
请注意,永远不需要 .end
指令。以大写 L
开头的标签是不以符号 table 结尾的局部标签,这就是 C 编译器喜欢使用它们的原因。