打印字符串的第一个字符导致分段错误
Print first character of string results in segmentation fault
我正在尝试使用 printf
在 64bit
Ubuntu 20
环境中将字符串 str_1
的第一个字符打印到 x86-assembly
中的标准输出,这是我的尝试:
; nasm -f test.asm && gcc -m32 -o test test.asm.o
section .text
global main
extern printf
some_proc:
mov esi, str_1
mov eax, [esi]
push eax
push argv_str
call printf
pop eax
ret
main:
call some_proc
ret
section .data
str_1 db `three`
argv_str db `%c\n`
这输出:
t
Segmentation fault (core dumped)
预期标准输出:
t
为什么此代码会导致分段错误以及如何修改代码以输出预期的标准输出?
您有几个错误:
您将两个 4 字节参数压入 printf
的堆栈。在 SysV 调用约定中,printf
会将它们留在那里,因此您有责任在之后调整堆栈以“删除”它们。记住 ret
会在栈顶寻找 return 地址;正如您的代码所代表的那样,您推送的 eax
中的字符值将会是什么。那不是一个有效的地址,因此尝试 return 会导致段错误。您可以通过 pop
ping 两次删除这些参数,或者更有效地通过简单地将 8
添加到 esp
,从而将堆栈指针移回原来的位置。
当前版本的 i386 SysV ABI 要求堆栈在 call
ing 任何函数之前对齐到 16 字节。考虑到 call
本身将 4 个字节压入堆栈作为 return 地址这一事实,就像每个 push
指令一样,您可以计算出调用 [=] 所需的必要调整21=] 和 printf
,并根据需要从 esp
中添加或减去。 (从技术上讲,您可以避免在调用 some_proc
之前对齐堆栈,而只是在 printf
之前修复它,但这太容易搞砸了。)一些 32 位库可能以这样的方式编译这个要求没有强制执行,但是64位代码肯定需要,所以遵守是个好习惯。
esi
是根据 i386 SysV ABI calling conventions 的 callee-saved 寄存器(记住这些!)。如果你想修改它,你必须保存以前的内容并在 returning 之前恢复它们(例如函数顶部的 push esi
和最后的 pop esi
)。或者选择 caller-saved 寄存器,例如 ecx
。但是,如下所述,您实际上根本不需要为 str1
地址使用寄存器。
mov eax, [esi]
是32位加载,因为eax
是32位寄存器。因此,这将使用位置 str_1
的 4 个字节加载 eax
,这将导致它包含值 0x65726874
(字节 t h r e
作为 little-endian 整数).这实际上可能不会导致问题,因为 printf
应该将其 int
参数转换回 unsigned char
以进行打印,因此您应该只获取低字节 0x74 = 't'
,但它仍然很奇怪,如果您的字符串很短并且与未映射的页面相邻,则可能会中断。
更安全的是 mov al, [esi]
,它只将一个字节加载到 al
,这是 eax
的低字节,但是高 3 字节中的任何垃圾都会留在那儿。您可以预先使用 xor eax, eax
将 eax
清零,但您也可以使用 movzx
指令用一块石头杀死两只鸟,其中 zero-extends 一个较小的操作数变成一个较大的操作数: movzx eax, byte [esi]
.
当然,先将地址放入esi
是多余的,因为地址可以指定为立即数:mov al, [str_1]
或movzx eax, byte [str_1]
。这样就避免了 save/restore esi
.
main
需要 return 退出代码,return 值总是进入 eax
。您的 eax
将包含您的角色或 printf
中的 return 值,具体取决于您的 push/pops 的最终位置。其中任何一个都是奇怪的非零退出代码,您的 shell 会认为程序遇到错误。因此,在从 main
进行 return 之前将 eax
清零以表示成功。
argv_str
是一个奇怪的字符串名称,与 argv
.
无关
我会修改你的程序如下:
; nasm -f test.asm && gcc -m32 -o test test.asm.o
section .text
global main
extern printf
some_proc:
sub esp, 4 ; 8 more bytes pushed before call to printf
movzx eax, byte [str_1]
push eax
push argv_str
call printf
add esp, 12
ret
main:
sub esp, 12
call some_proc
xor eax, eax
add esp, 12
ret
section .data
str_1 db `three`
argv_str db `%c\n`
我正在尝试使用 printf
在 64bit
Ubuntu 20
环境中将字符串 str_1
的第一个字符打印到 x86-assembly
中的标准输出,这是我的尝试:
; nasm -f test.asm && gcc -m32 -o test test.asm.o
section .text
global main
extern printf
some_proc:
mov esi, str_1
mov eax, [esi]
push eax
push argv_str
call printf
pop eax
ret
main:
call some_proc
ret
section .data
str_1 db `three`
argv_str db `%c\n`
这输出:
t
Segmentation fault (core dumped)
预期标准输出:
t
为什么此代码会导致分段错误以及如何修改代码以输出预期的标准输出?
您有几个错误:
您将两个 4 字节参数压入
printf
的堆栈。在 SysV 调用约定中,printf
会将它们留在那里,因此您有责任在之后调整堆栈以“删除”它们。记住ret
会在栈顶寻找 return 地址;正如您的代码所代表的那样,您推送的eax
中的字符值将会是什么。那不是一个有效的地址,因此尝试 return 会导致段错误。您可以通过pop
ping 两次删除这些参数,或者更有效地通过简单地将8
添加到esp
,从而将堆栈指针移回原来的位置。当前版本的 i386 SysV ABI 要求堆栈在
call
ing 任何函数之前对齐到 16 字节。考虑到call
本身将 4 个字节压入堆栈作为 return 地址这一事实,就像每个push
指令一样,您可以计算出调用 [=] 所需的必要调整21=] 和printf
,并根据需要从esp
中添加或减去。 (从技术上讲,您可以避免在调用some_proc
之前对齐堆栈,而只是在printf
之前修复它,但这太容易搞砸了。)一些 32 位库可能以这样的方式编译这个要求没有强制执行,但是64位代码肯定需要,所以遵守是个好习惯。esi
是根据 i386 SysV ABI calling conventions 的 callee-saved 寄存器(记住这些!)。如果你想修改它,你必须保存以前的内容并在 returning 之前恢复它们(例如函数顶部的push esi
和最后的pop esi
)。或者选择 caller-saved 寄存器,例如ecx
。但是,如下所述,您实际上根本不需要为str1
地址使用寄存器。mov eax, [esi]
是32位加载,因为eax
是32位寄存器。因此,这将使用位置str_1
的 4 个字节加载eax
,这将导致它包含值0x65726874
(字节t h r e
作为 little-endian 整数).这实际上可能不会导致问题,因为printf
应该将其int
参数转换回unsigned char
以进行打印,因此您应该只获取低字节0x74 = 't'
,但它仍然很奇怪,如果您的字符串很短并且与未映射的页面相邻,则可能会中断。更安全的是
mov al, [esi]
,它只将一个字节加载到al
,这是eax
的低字节,但是高 3 字节中的任何垃圾都会留在那儿。您可以预先使用xor eax, eax
将eax
清零,但您也可以使用movzx
指令用一块石头杀死两只鸟,其中 zero-extends 一个较小的操作数变成一个较大的操作数:movzx eax, byte [esi]
.当然,先将地址放入
esi
是多余的,因为地址可以指定为立即数:mov al, [str_1]
或movzx eax, byte [str_1]
。这样就避免了 save/restoreesi
.main
需要 return 退出代码,return 值总是进入eax
。您的eax
将包含您的角色或printf
中的 return 值,具体取决于您的 push/pops 的最终位置。其中任何一个都是奇怪的非零退出代码,您的 shell 会认为程序遇到错误。因此,在从main
进行 return 之前将eax
清零以表示成功。
无关argv_str
是一个奇怪的字符串名称,与argv
.
我会修改你的程序如下:
; nasm -f test.asm && gcc -m32 -o test test.asm.o
section .text
global main
extern printf
some_proc:
sub esp, 4 ; 8 more bytes pushed before call to printf
movzx eax, byte [str_1]
push eax
push argv_str
call printf
add esp, 12
ret
main:
sub esp, 12
call some_proc
xor eax, eax
add esp, 12
ret
section .data
str_1 db `three`
argv_str db `%c\n`