我正在尝试在 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