无法从汇编(yasm)代码调用 64 位 Linux 上的 C 标准库函数
- 2024-09-30 14:02:00
- admin 原创
- 88
问题描述:
我有一个用汇编语言编写的函数foo
,在 Linux (Ubuntu) 64 位上用 yasm 和 GCC 编译。它只是使用 将消息打印到 stdout puts()
,如下所示:
bits 64
extern puts
global foo
section .data
message:
db 'foo() called', 0
section .text
foo:
push rbp
mov rbp, rsp
lea rdi, [rel message]
call puts
pop rbp
ret
它由用 GCC 编译的 C 程序调用:
extern void foo();
int main() {
foo();
return 0;
}
构建命令:
yasm -f elf64 foo_64_unix.asm
gcc -c foo_main.c -o foo_main.o
gcc foo_64_unix.o foo_main.o -o foo
./foo
问题如下:
运行程序时,它会打印一条错误消息,并在调用期间立即发生段错误puts
:
./foo: Symbol `puts' causes overflow in R_X86_64_PC32 relocation
Segmentation fault
使用 objdump 反汇编后,我发现调用了错误的地址:
0000000000000660 <foo>:
660: 90 nop
661: 55 push %rbp
662: 48 89 e5 mov %rsp,%rbp
665: 48 8d 3d a4 09 20 00 lea 0x2009a4(%rip),%rdi
66c: e8 00 00 00 00 callq 671 <foo+0x11> <-- here
671: 5d pop %rbp
672: c3 retq
(671 是下一条指令的地址,而不是的地址puts
)
但是,如果我用 C 语言重写相同的代码,调用方式会有所不同:
645: e8 c6 fe ff ff callq 510 <puts@plt>
即,它引用puts
了 PLT。
是否可以告诉 yasm 生成类似的代码?
解决方案 1:
TL:DR: 3 个选项:
构建一个非 PIE 可执行文件(
gcc -no-pie -fno-pie call-lib.c libcall.o
),以便当您写入时,链接器将为您透明地生成 PLT 条目call puts
。call puts wrt ..plt
就像gcc -fPIE
会做的那样。call [rel puts wrt ..got]
就像gcc -fno-plt
会做的那样。
后两种方法可以在 PIE 可执行文件或共享库中使用。第三种方法wrt ..got
效率略高一些。
您的 gcc 默认构建 PIE 可执行文件(x86-64 Linux 不再允许使用 32 位绝对地址?)。
我不知道为什么,但这样做时,链接器不会自动解析call puts
为call puts@plt
。仍然会puts
生成一个 PLT 条目,但call
不会到达那里。
在运行时,动态链接器尝试puts
直接解析该名称的 libc 符号并修复call rel32
。但符号距离超过 +-2^31,因此我们收到有关重R_X86_64_PC32
定位溢出的警告。目标地址的低 32 位是正确的,但高位不正确。(因此您call
跳转到了错误的地址)。
如果我使用 构建,您的代码对我有用gcc -no-pie -fno-pie call-lib.c libcall.o
。这-no-pie
是关键部分:它是链接器选项。您的 YASM 命令不必更改。
当创建传统的位置相关可执行文件时,链接器会将puts
调用目标的符号转换为puts@plt
,因为我们正在链接动态可执行文件(而不是将 libc 与 静态链接gcc -static -fno-pie
,在这种情况下call
可以直接转到 libc 函数。)
无论如何,这就是为什么 gcccall puts@plt
在使用 编译时会发出(GAS 语法) (这是桌面上的默认设置,但不是<https://godbolt.org/>-fpie
上的默认设置),但仅在使用 编译时发出。call puts
`-fno-pie`
有关 PLT 的更多信息,请参阅此处的 @plt 是什么意思?以及几年前Linux 上动态库的糟糕状态gcc -fno-plt
。(现代就像那篇博客文章中的一个想法。)
顺便说一句,更准确/具体的原型可以让 gcc 避免在调用之前将 EAX 清零foo
:
extern void foo();
在 C 中意味着extern void foo(...);
您可以将其声明为extern void foo(void);
,这就是()
在 C++ 中的意思。C++ 不允许未指定参数的函数声明。
asm 改进
您还可以输入message
(section .rodata
只读数据,链接为文本段的一部分)。
您不需要堆栈框架,只需要在调用之前将堆栈对齐 16。虚拟函数push rax
就可以完成此操作。
或者我们可以puts
通过跳转到尾部调用而不是调用它来进行尾部调用,堆栈位置与进入此函数时相同。无论有没有 PIE,这都可以工作。只要 RSP 指向您自己的返回地址,只需将其替换call
为即可。jmp
如果你想制作 PIE 可执行文件(或共享库),你有两个选择
call puts wrt ..plt
- 明确通过 PLT 调用。call [rel puts wrt ..got]
- 明确地通过 GOT 条目进行间接调用,就像 gcc 的-fno-plt
代码生成风格一样。(使用 RIP 相对寻址模式到达 GOT,因此使用关键字rel
)。
WRT = 尊重。NASM 手册文档wrt ..plt
,另请参阅第 7.9.3 节:特殊符号和 WRT。
default rel
通常情况下,您会在文件顶部使用,这样您就可以实际使用call [puts wrt ..got]
并仍然获得 RIP 相对寻址模式。您不能在 PIE 或 PIC 代码中使用 32 位绝对寻址模式。
call [puts wrt ..got]
使用动态链接存储在 GOT 中的函数指针汇编为内存间接调用。(早期绑定,而非惰性动态链接。)
NASM 文档中..got
有关获取变量地址的说明请参见第 9.2.3 节。(其他)库中的函数是相同的:您从 GOT 获取指针,而不是直接调用,因为偏移量不是链接时常量,可能不适合 32 位。
YASM 也接受call [puts wrt ..GOTPCREL]
,如 AT&T 语法call *puts@GOTPCREL(%rip)
,但 NASM 不接受。
; don't use BITS 64. You *want* an error if you try to assemble this into a 32-bit .o
default rel ; RIP-relative addressing instead of 32-bit absolute by default; makes the [rel ...] optional
section .rodata ; .rodata is best for constants, not .data
message:
db 'foo() called', 0
section .text
global foo
foo:
sub rsp, 8 ; align the stack by 16
; PIE with PLT
lea rdi, [rel message] ; needed for PIE
call puts WRT ..plt ; tailcall puts
;or
; PIE with -fno-plt style code, skips the PLT indirection
lea rdi, [rel message]
call [rel puts wrt ..got]
;or
; non-PIE
mov edi, message ; more efficient, but only works in non-PIE / non-PIC
call puts ; linker will rewrite it into call puts@plt
add rsp,8 ; restore the stack, undoing the add
ret
在位置相关的Linux 可执行文件中,您可以使用mov edi, message
RIP 相对 LEA 来代替。它的代码大小更小,可以在大多数 CPU 上的更多执行端口上运行。(有趣的事实:MacOS 总是将“映像基数”放在低 4GiB 之外,因此这种优化在那里是不可能的。)
在非 PIE 可执行文件中,您也可以使用call puts
或jmp puts
让链接器进行排序,除非您想要更高效的无 plt 样式动态链接。但如果您确实选择静态链接 libc,我认为这是您直接跳转到 libc 函数的唯一方法。
(我认为非 PIE 静态链接的可能性就是为什么 ld
愿意为非 PIE 自动生成 PLT 存根,但不会为 PIE 或共享库生成。它要求您在链接 ELF 共享对象时说明您的意思。)
call puts
如果你确实在 PIE 中使用( call rel32
),那么只有将位置独立的实现静态链接puts
到 PIE 中时,它才能工作,因此整个东西是一个可执行文件,它将在运行时加载到随机地址(通过通常的动态链接器机制),但根本不依赖于libc.so.6
当目标在静态链接时存在时,链接器“放松”调用
GAScall *bar@GOTPCREL(%rip)
用途R_X86_64_GOTPCRELX
(可放宽)
NASMcall [rel bar wrt ..got]
用途R_X86_64_GOTPCREL
(不可放宽)
对于手写 asm 来说,这不是什么问题;call bar
当你知道符号将出现在你要链接的另一个符号中.o
(而不是.so
)时,你可以直接使用。但是 C 编译器不知道库函数和你用原型声明的其他用户函数之间的区别(除非你使用gcc -fvisibility=hidden
<https://gcc.gnu.org/wiki/Visibility>或属性/指令之类的东西)。
不过,如果您静态链接库,您可能希望编写链接器可以优化的 asm 源,但据我所知,您无法使用 NASM 做到这一点。您可以使用 将符号导出为隐藏符号(在静态链接时可见,但在最终 .so 中动态链接时不可见)global bar:function hidden
,但那是在定义函数的源文件中,而不是访问它的文件中。
global bar
bar:
mov eax,231
syscall
call bar wrt ..plt
call [rel bar wrt ..got]
extern bar
第二个文件,在使用进行汇编nasm -felf64
和反汇编之后objdump -drwc -Mintel
,可以看到重定位:
0000000000000000 <.text>:
0: e8 00 00 00 00 call 0x5 1: R_X86_64_PLT32 bar-0x4
5: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # 0xb 7: R_X86_64_GOTPCREL bar-0x4
ld
与(GNU Binutils) 2.35.1链接后-ld bar.o bar2.o -o bar
0000000000401000 <_start>:
401000: e8 0b 00 00 00 call 401010 <bar>
401005: ff 15 ed 1f 00 00 call QWORD PTR [rip+0x1fed] # 402ff8 <.got>
40100b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
0000000000401010 <bar>:
401010: b8 e7 00 00 00 mov eax,0xe7
401015: 0f 05 syscall
请注意,PLT 形式被放宽为直接的call bar
,PLT 被消除。但ff 15
call [rel mem] 并没有被放宽为e8 rel32
使用 GAS:
_start:
call bar@plt
call *bar@GOTPCREL(%rip)
gcc -c foo.s && disas foo.o
0000000000000000 <_start>:
0: e8 00 00 00 00 call 5 <_start+0x5> 1: R_X86_64_PLT32 bar-0x4
5: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # b <_start+0xb> 7: R_X86_64_GOTPCRELX bar-0x4
注意 R_X86_64_GOTPCRELX 末尾的 X
ld bar2.o foo.o -o bar && disas bar
。:
0000000000401000 <bar>:
401000: b8 e7 00 00 00 mov eax,0xe7
401005: 0f 05 syscall
0000000000401007 <_start>:
401007: e8 f4 ff ff ff call 401000 <bar>
40100c: 67 e8 ee ff ff ff addr32 call 401000 <bar>
两次调用都放宽为直接e8
call rel32
到目标地址。间接调用中的额外字节用67
地址大小前缀填充(对没有影响call rel32
),将指令填充到相同的长度。(因为重新组装和重新计算函数内的所有相关分支以及对齐等已经太晚了。)
call *puts@GOTPCREL(%rip)
如果您静态链接 libc,就会发生这种情况gcc -static
。
解决方案 2:
操作码0xe8
后跟一个有符号偏移量,该偏移量将应用于 PC(此时 PC 已前进到下一条指令)以计算分支目标。因此objdump
将分支目标解释为0x671
。
YASM 渲染为零,因为它可能在该偏移量上放置了一个重定位,这就是它要求加载器puts
在加载期间填充正确偏移量的方式。加载器在计算重定位时遇到溢出,这可能表明它puts
与您的调用之间的偏移量比 32 位有符号偏移量所能表示的偏移量更远。因此,加载器无法修复此指令,导致崩溃。
66c: e8 00 00 00 00
显示未填充的地址。如果您查看重定位表,您应该会看到重定位0x66d
。汇编程序将重定位的地址/偏移量填充为全零并不罕见。
此页面表明 YASM 有一个可以控制、等WRT
使用的指令。.got
`.plt`
根据NASM 文档中的 S9.2.5 ,看起来您可以使用CALL puts WRT ..plt
(假设 YASM 具有相同的语法)。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件