在 NASM 程序集中调用函数时出现分段错误



我正在尝试在 nasm 中调用我自己的函数,它工作了 2 次,然后它给出了分段错误错误。我创建了两个函数 display1 和 display2,它们将分别显示"这是消息 1"和"这是消息 2"。这些函数首次正确,但在调用这些函数两次时显示分段错误。

global _start
section .text
display1:
mov eax, 0x4
mov ebx, 0x1
mov ecx, var1
mov edx, len1
int 0x80
ret
display2:
mov eax, 0x4
mov ebx, 0x1
mov ecx, var2
mov edx, var2
int 0x80
ret
_start:
call display1
call display2
call display1
call display2
mov eax, 0x1
mov ebx, 0x5
int 0x80
section .data
var1: db "This is message1", 0x0A, 0x00
len1 equ $-var1
var2: db "This is message2", 0x0A, 0x00
len2 equ $-var2
This is message1
This is message2
.symtab.strtab.shstrtab.text.data�N!�$�'
�
�U�����"'��,1��6�=���I���P���functions.nasmdisplay1display2var1len1var2len2_start__bss_start_edata_endThis is message1
This is message2
.symtab.strtab.shstrtab.text.data�N!�$�'
�
�U�����"'��,1��6�=���I���P���functions.nasmdisplay1display2var1len1var2len2_start__bss_start_edata_endSegmentation fault (core dumped)

恭喜,你发现了一个内核错误(在你的非常旧的 Ubuntu 12.04/Linux 3.13.0-32 通用 32 位内核中(。

mov edx, var2传递一个非常大的整数(地址(作为大小。 这就是为什么您在第二条消息之后收到垃圾的原因;write系统调用是将内存读取到未映射页面附近的某个位置,然后停止。

在无缺陷的内核上,write返回和执行将继续,直到_exit系统调用如您所期望的那样。

指令int 0x80导致分段错误。

IDK是否比破坏用户空间并导致以后的错误更疯狂或更不疯狂。

在任何地方报告这个内核错误可能都不值得。Ubuntu 12.04 LTS 在 2017 年达到生命周期结束。 该错误在现代内核中不存在,并且可能是偶然发现或修复的,作为自该内核当前以来的 7 年中其他一些更改的一部分。


在带有 write(( 的非错误内核中会发生什么,最终从未映射的页面读取

write(2)手册页绝对没有记录在错误参数上发出信号的可能性,只记录了像EFAULT这样的错误代码。

我无法使用 x86-64 Linux 内核 5.0.1 在 Arch Linux 上重现段错误;我得到了预期的垃圾,然后write(2)返回在到达未映射页面之前写入的字节数。 然后继续执行,直到_exit(5)系统调用并且进程干净退出,状态 = 5。

我认为当您传递包含未映射页面的指针+大小时,即使在写入一些字节后write也可能会返回-EFAULT,但事实并非如此。 手册页中的措辞没有提到这种特定情况,但如何处理在编写过程中检测到的其他错误的措辞与此一致。 (通常这些错误来自磁盘变满,或者管道的另一侧关闭。

write(2)Linux 手册页

请注意,成功的 write(( 传输的字节数可能少于计数。 这种部分写入可能由于各种原因而发生;...

如果发生 部分写入,调用方可以进行另一个 write(( 调用来传输剩余的字节。 后续调用将传输更多字节 或者可能导致错误(例如,如果磁盘现在已满(。

当你这样做时,Linux 绝对不会一直传输到最后一个映射页面的末尾。 但有趣的是,看看不同情况下会发生什么。

它似乎以块的形式复制,并检查每个块的可读性。 当块从未映射的页面读取时,会检测到错误,并返回部分写入。 如果你再和address = buf + first_retval打个电话,你可能会得到一个-EFAULT。 因此,这很像用部分写入填充磁盘,然后在尝试写入其余部分时通过-ENOSPC来检测它。

将输出重定向到 x86-64 Linux 5.0.1 上的文件(tmpfs(,我得到 4078write()大小。4096-18 = 4078,并且我使用的是最近的ld(Binutils 2.32(,因此.data部分在可执行文件中是 4k 对齐的,并且该部分的开头也在内存中按页面对齐。 所以页面的结尾是var2 + 4096 - len1.

$ strace ./2write > foo
strace: [ Process PID=28961 runs in 32 bit mode. ]
write(1, "This is message1n", 18)    = 18
write(1, "This is message2n"..., 134520850) = 4078
write(1, "This is message1n", 18)    = 18
write(1, "This is message2n"..., 134520850) = 4078
exit(5)                                 = ?
+++ exited with 5 +++

与写入终端相比,我得到的大小为2048

与写给/dev/null,我写回134520850就成功了。null特殊块设备的驱动程序甚至不读取用户空间内存,它只是从write系统调用中返回成功。 所以没有什么会检查-EFAULT.

将输出管道输送到wc,我在第一个错误的调用中得到了令人惊讶的 18 字节部分写入,并在下一个电话中得到了-EFAULT

strace ./2write | wc
execve("./2write", ["./2write"], 0x7ffdba771cf0 /* 53 vars */) = 0
strace: [ Process PID=29008 runs in 32 bit mode. ]
write(1, "This is message1n", 18)    = 18
write(1, "This is message2n"..., 134520850) = 18
write(1, "This is message1n", 18)    = 18
write(1, "This is message2n"..., 134520850) = -1 EFAULT (Bad address)
exit(5)                                 = ?
+++ exited with 5 +++
3       9      54

在随后的程序运行中,我立即-EFAULT。 我猜 Linux 可能在第一次调用后为管道缓冲区分配了更多内存,因此在复制任何数据之前,它能够向前看足够远,立即注意到错误的地址。

peter@volta:/tmp$ strace ./2write | wc
execve("./2write", ["./2write"], 0x7fff868a41b0 /* 53 vars */) = 0
strace: [ Process PID=29015 runs in 32 bit mode. ]
write(1, "This is message1n", 18)    = 18
write(1, "This is message2n"..., 134520850) = -1 EFAULT (Bad address)
write(1, "This is message1n", 18)    = 18
write(1, "This is message2n"..., 134520850) = -1 EFAULT (Bad address)
exit(5)                                 = ?
2       6      36

最新更新