为什么这些 const int main=0xc3(或其他数字)程序 return 252 在 OS X 上?
Why do these `const int main=0xc3` (or other number) programs return 252 on OS X?
我听说 "shortest C program that results in an illegal instruction": const main=6;
for x86-64 over on codegolf.SE 这让我很好奇如果我把不同的数字放在那里会发生什么。
现在我想这与什么是或不是有效的 x86-64 指令 (durr) 有关,但具体来说,我想知道不同结果的含义。
const main=0
到 2
给出总线错误。
const main=3
给出段错误。
6
和 7
给出了非法指令。
我收到各种总线错误、段错误和非法指令,直到
const main=194
根本没有打断我(至少没有打断我生成这些小程序的 python 脚本)。
还有一些其他数字也不会导致 exceptions/interrupts,因此不会导致 Unix 信号。我查了一对夫妇的return密码,return密码是252
。我不知道为什么或那意味着什么或它是如何到达那里的。
204
给了我一个 "trace trap"。这是 0xcc
,我知道这是 int3
中断 - 这很有趣! (241/0xf1 也让我得到这个)
无论如何,它一直在继续,显然主要是总线错误和段错误以及一些非法指令,偶尔...做任何事情然后 returns 252...
我在谷歌上搜索了一些操作码,但我真的不知道我在做什么,或者老实说该去哪里寻找。我什至还没有查看我所有的输出,只是一直在滚动浏览。我知道段错误是对有效内存的无效访问,而总线错误是对无效内存的访问,我计划查看数字的模式并找出这些问题发生的位置和原因。但是252的事情让我有点难过。
#!/usr/bin/env python3
import os
import subprocess
import time
import signal
os.mkdir("testc")
try:
os.chdir("testc")
except:
print("Could not change directory, exiting.")
for i in range(0, 65536):
filename = "test" + str(i) + ".c"
f = open(filename, "w")
f.write("const main=" + str(i) + ";")
f.close()
outname = "test" + str(i)
subprocess.Popen(["gcc", filename, "-o", outname], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(1)
err = subprocess.Popen("./" + outname, shell=True)
result = None
while result is None:
result = err.poll()
r = result
if result == -11:
r = "segfault"
if result == -10:
r = "bus error"
if result == -4:
r = "illegal instruction"
if result == -5:
print = "trap"
print("const main=" + str(hex(i)) + " : " + r)
这会在 testc/test20.c
中生成一个 C 程序,如
const int main=20;
然后用gcc
编译并运行。 (并在尝试下一个数字之前休眠 1 秒。)
没有期望。我只是想看看发生了什么。
int main = 194
是 c2 00 00 00
,解码为 ret 0
无论调用什么main
,都必须在RAX的低字节中留下252。 (调用约定说 RAX 是 return-value 寄存器,但它不是 arg-passing 寄存器,所以在函数入口上它包含调用者使用它的任何 tmp 垃圾。)
请参阅答案底部的理论,了解为什么 SIGBUS 代表 2 而 SIGSEGV 代表 3:我认为 RAX 是进入 main 的有效指针(偶然动态链接器在那里的内容),03 00 add eax, [rax]
销毁它但 02 00 add al, [rax]
没有,然后从 main
的下 2 个字节开始在 00 00 add [rax], al
上执行任一错误,或者运行 00 00
指令然后从页面末尾掉下来。
:RAX是指向main
(在read-only TEXT段),存储到read-only 页面也有 SIGBUS。 所以如果 RAX 仍然指向那里,00 00 add [rax], al
也会因为这个原因而 SIGBUS。
(请注意,这个答案有一些错误的猜测,并且每次我从@SWilliams 或@MichaelPetch 获得新信息时都没有完全重写。要点是关于哪种 #PF
导致哪个信号上升到目前为止,我已经尝试至少在不太准确的事情之后添加一个更正。我认为错误的理论有一些价值,作为其他类型事情的例证可能 已经发生,所以我将其全部留在此处。)
你的 Python 程序在我的 Linux 机器上失败,一旦它到达 c2 00 00 00
ret imm16
,第一个 return 成功。 (在 Linux 上,.rodata
部分在 之后 .text
in the TEXT segment 结束,所以 main
没有任何东西可以下降入。)
...
const main=0xc0 : segfault
const main=0xc1 : segfault
Traceback (most recent call last):
File "./opcode-test.py", line 34, in <module>
print("const main=" + str(hex(i)) + " : " + r)
TypeError: must be str, not int
难道 python 没有 strsignal(3)
的等价物来将信号映射到标准文本字符串,例如 "Illegal instruction"? (像 strerror
但信号代码而不是 errno 值?)
大多数 x86 指令都是多字节长。 x86 是 little-endian,所以您主要查看
?? 00 00 00 90 90 90 ...
或更大的整数 ?? ?? 00 00 90 90 90 90 ...
,假设您的链接器使用 0x90 nop
填充函数之间的字节,就像 Linux 上的 GNU ld
那样。
这些字节序列可能会在您命中 NOP 之前解码为一条或多条有效指令,并落入链接器在 main
之后放置的任何 CRT 函数。如果你没有错误地到达那里,并且没有偏移堆栈指针,那么你已经进入了堆栈上具有有效 return 地址的函数(main
的调用者,另一个 CRT 函数)就像 main tail-called它。
大概是函数 returns 252(或一些低字节为 252 的更宽的值)。从 main
返回会导致干净的进程退出,使用 main
的 return 值进行退出系统调用。
这个 fall-through 尾调用就像 main 以 return next_function(argc, argv);
结束一样。
更正(没有重写整个答案,抱歉)
因为 main=194
是第一个有效的,我认为你实际上并没有得到 fall-through,可能只有 C2 ret imm16
和 C3 ret
通向干净的出口。对于 c2
,它必须后跟 2 00
个字节,否则它会破坏 main
的调用者的堆栈。
或那些带有不做任何事情的前缀的指令,或无害的one-byte指令。例如90 nop
/ c3 ret
或 90 nop
/ c2 00 00 ret 0
。或者 91 xchg eax, ecx
等实际上可以给你一个不同的 return 值,用另一个寄存器交换 EAX。 (x86 将操作码 90 .. 97
专用于 xchg-with-EAX,因为在原始 8086 AX 上更多 "special",没有像 movsx
到 sign-extend 这样的指令进入其他寄存器。并且没有 2操作数 imul
.
其他无害的one-byte指令包括99 cdq
和98 cwde
,但不包括push
或pop
(因为更改 RSP 会使它不指向 return 地址)。一些 one-byte 标志 set/clear 指令是 f9 stc
、fd std
,但不是 fb sti
(这是特权,与进位标志和方向标志不同)。
无害前缀是 0x40..4f
REX 前缀、0xf2/
f3REP, and
0x66and
0x67` operand-size 和地址大小。此外,任何 segment-override 前缀也可能是无害的。
我刚刚测试了 main=0xc366
和 main=0xc367
是的,它们都干净地退出了。 GDB 将 66 c3
解码为 retw
(operand-size 前缀)并将 67 c3
解码为 addr32 ret
(地址大小前缀),但两者仍弹出 64 位 return 地址,也不要截断堆栈指针。 (我取出了我一直在使用的 -no-pie
,所以 RIP 和 RSP 一起在低 32 位之外)。
注意 00
是 add [r/m8], r8
的操作码,所以 00 00
解码为 add [rax], al
.
要通过那些 00
字节并到达链接器作为填充插入的 "nop sled",您需要操作码(如果操作码使用一个,则需要 modrm 字节)编码较长指令的开始 ,例如 0xb8 mov eax, imm32
,它有 5 个字节长,并消耗 0xb8
之后的下一个 4 个字节。其实还有short-formmov-immediate 每个寄存器的编码,所以 0xb8 + 0..7
都会让你跨越差距。除了mov esp, imm32
,一旦执行到下一个函数就会崩溃,因为它踩到了堆栈指针。
早期的一个是 05
,add eax, imm32
的 short-form(无 modrm)操作码。大多数原始 8086 ALU 指令具有特殊的 AX,imm16 / EAX,imm32 短格式,而不是使用 ModRM 字节编码目标操作数的 op r/m32, imm32
或 imm8
格式。 (并且 ModRM 中 /r
字段的位作为额外的操作码位。)
有关 AL/EAX/RAX 短格式编码和单字节指令的更多信息,请参见Tips for golfing in x86/x64 machine code。
手动解码x86机器码,看Intel的手册,特别是vol.2手册,详细说明了指令编码格式,最后有一个操作码table。 (参见链接 in the x86 tag wiki). For just an opcode map, see http://ref.x86asm.net/coder64.html.
使用反汇编器或调试器查看您的 executables
中有什么
但实际上,使用像 objdump -drwC -Mintel
这样的反汇编程序。或者 llvm-objdump
。在输出中找到 main
,然后查看得到的结果。 (或者使用 GDB,因为指令中间的标签会使反汇编器失效。)
使用objdump -rwC -Mintel -D -j .rodata -j .text testc/test194
得到这样的输出,将.text和.rodata部分反汇编为代码:
testc/test194: file format elf64-x86-64
Disassembly of section .text:
0000000000400540 <__libc_csu_init>:
400540: 41 57 push r15
400542: 49 89 d7 mov r15,rdx
...
4005a4: c3 ret
4005a5: 90 nop
4005a6: 66 2e 0f 1f 84 00 00 00 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
00000000004005b0 <__libc_csu_fini>:
4005b0: c3 ret
Disassembly of section .rodata:
00000000004005c0 <_IO_stdin_used>: ;;;; This is actually data!
4005c0: 01 00 add DWORD PTR [rax],eax
4005c2: 02 00 add al,BYTE PTR [rax]
00000000004005c4 <main>:
4005c4: c2 00 00 ret 0x0
... ; objdump elided the last 0, not me. It literally put ...
(我修改了你的 python 脚本以添加 -no-pie
gcc 选项,这就是为什么我的反汇编具有绝对地址,而不仅仅是相对于文件开头的小地址 = 0。我想知道这是否会把 main
放在它可能掉下来的地方,但它没有。)
请注意,.text 和 .rodata 之间只有很小的差距。它们属于同一 ELF 段(在 OS 的程序加载器查看的 ELF 程序头中),因此它们属于同一映射,它们之间没有未映射的页面。 如果幸运的话,中间的字节甚至用 0x90 nop
填充而不是 00
。实际上,有些东西用长 NOP 填补了 __libc_csu_init
和 __libc_csu_fini
之间的空白。如果它们在同一个源文件中,可能是来自汇编程序。
main
当然在 .rodata
中,因为您在 C 中将其声明为 read-only 全局(静态存储),如 const int main = 6;
。我你用了 const int main __attribute__((section(".text"))) = 123
,你可以在正常的 .text
部分得到 main
。在我的系统上,它在 __libc_csu_init
.
之前结束
但是标签会中断反汇编;反汇编器认为它一定是错误的并从标签重新开始解码。所以在 testc/test5
上的 GDB 中(使用 set disassembly-flavor intel
和 layout reg
,然后使用 start
命令在 main
开始时停止),我会得到
|0x40053c <main> add eax,0x41000000 │
│0x400541 <__libc_csu_init+1> push rdi │
│0x400542 <__libc_csu_init+2> mov r15,rdx
但是从 objdump -drwC -Mintel
开始(仅反汇编 .text
部分是 -d
的默认设置,我使用 GNU C 属性将 main
放在那里所以我的程序可以按照你的方式工作),我得到:
000000000040053c <main>:
40053c: 05 00 00 00 ....
0000000000400540 <__libc_csu_init>:
400540: 41 57 push r15
400542: 49 89 d7 mov r15,rdx
请注意,与 05 00 00 00
位于同一行的 ....
表示解码没有到达指令的末尾。
并且由于 main
在这里没有按 16 对齐,所以它恰好与 __libc_csu_init
的开头对齐。因此 add eax, imm32
从 push r15
消耗 REX.W 前缀(41
),如果通过从 main 掉落而不是通过调用到达,则将其解码为 push rdi
到 __libc_csu_init
标签。
以上输出来自Linux。您的 OS X 系统会有所不同
OS X 将大部分 CRT 启动代码放在 libc 中,而不是静态链接到 executable 和 main
.
或者也许您的主线没有任何东西可以落入
如果有,main=5
会起作用,但你说第一个 non-crashing 结果是 main=194
,这是一个实际的 ret
.
如果在 c3 ret
或 c2 00 00 ret 0
returned 之前没有任何东西,那么在 main
之后可能没有什么可落入的,或者间隙没有用重复的 [ 填充=48=] 形成一个 "nop sled" 如果解码从中间的任何地方开始,它将执行正常。 (例如,在较早的指令消耗了双字 int main
末尾的尾随 0
字节和一些填充字节之后。)
I understand that a segfault is invalid access to valid memory and a bus error is access to invalid memory
不,那个简化的描述是倒退的。通常你会在所有 Unix 上尝试访问未映射的页面时遇到段错误。但是对于某些类型的无效访问(即使在有效地址上),您会收到总线错误。
SPARC 上的 Solaris 为未对齐字 loads/stores 到有效内存提供总线错误。
在 x86-64 Linux 上,您只会在非常奇怪的情况下获得 SIGBUS。参见 Debugging SIGBUS on x86 Linux。 Non-canonical 导致 #SS
异常的堆栈指针,读取超过被截断的 mmap
ed 文件的末尾。此外,如果您启用 x86 对齐检查(AC 标志),但没有人这样做,因为像 memcpy 这样的库函数使用未对齐的 loads/stores,并且编译器 code-gen 假定未对齐的整数 loads/stores 是安全的。
IDK 什么硬件异常 *BSD 映射到 SIGBUS,但我假设常规 out-of-bounds 访问,如 NULL-pointer 取消引用,将 SIGSEGV挺标准的。
@MichaelPetch 在评论中说 on OS X
#PF
(page fault hardware exception) 来自code-fetch 例内核传递SIGBUS
#PF
从数据 load/store 到未映射的页面导致 SIGSEGV。
#PF
从商店到 read-only 页面导致 SIGBUS。 (并且 this 是 02 00 add al, [rax]
之后发生的事情,在 00 00 add [rax], al
中形成 main
的第二个字节。这个答案的其余部分没有考虑到这一点。)
(当然,这是在检查 page-fault 是否是由于硬件页面 table 和逻辑进程内存映射之间的差异,例如来自惰性映射 copy-on-write,或页面调出到磁盘。)
因此,如果您的 int main
位于未映射页面的末尾,05 add eax,imm32
会在包含 [=132= 的双字的末尾读取一个额外的字节](GAS 语法汇编中的 .long 5
)。 这将进入下一页和 SIGBUS。(您最后的评论表明它确实是 SIGBUS。)
关于前几个值的理论:
您举报:
- main =
02 00 add al, [rax]
/ `00 00 add [rax], al 的总线错误
- 但是 main =
03 00 add eax, [rax]
/ 00 00 add [rax], al
. 的段错误
我们知道RAX的低字节是252
,所以如果RAX持有一个有效的指针值,它就是4字节对齐的。因此,如果从 [rax]
加载一个字节有效,那么加载一个双字也是如此。
所以很可能memory-sourceadd
是成功的,并且修改了AL,RAX的低字节(字节操作数大小)大概还是留给RAX一个有效指针。** 然后,如果包含 main
的页面的其余部分充满了 00 00 add [rax], al
指令(或者只是 main 本身内部的指令),这些指令将成功(无需进一步修改 RAX),直到执行失败进入未映射的页面,只要 RAX 仍然 是 运行 之后的有效指针,无论 main
解码为什么。
实际上,memory-destination add
本身出现故障并引发 SIGBUS。
03 00 add eax, [rax]
写入 EAX,从而将 RAX 截断为 32 位。(将 32 位寄存器隐式 zero-extends 写入完整的 64-位寄存器,不像写低 8 位或 16 位部分寄存器。)这肯定会给你一个无效的指针,因为 OS X 映射静态 code/data 在低 32 位之外虚拟地址 space.
所以 下面的 00 00 add [rax], al
肯定会因尝试写入 out-of-bounds 地址而导致 出错,导致引发 SIGSEGV 的 #PF
。
可能只是一页结束前main
的最后两个字节00 00
00 00
。 否则 05 add eax, imm32
会因截断 RAX 而发生段错误,然后 运行 00 00 add [rax], al
。对于 SIGBUS,它必须 code-fetch 进入一个未映射的页面,之后不解码任何 memory-access 指令。
对于您所看到的肯定还有其他合理的解释,但我认为这可以解释您到目前为止的所有观察结果;没有更多数据,我们无法反驳它。显然,最简单的方法是启动 GDB 或任何其他调试器,只需 start
/ si
并观察会发生什么。
我听说 "shortest C program that results in an illegal instruction": const main=6;
for x86-64 over on codegolf.SE 这让我很好奇如果我把不同的数字放在那里会发生什么。
现在我想这与什么是或不是有效的 x86-64 指令 (durr) 有关,但具体来说,我想知道不同结果的含义。
const main=0
到2
给出总线错误。const main=3
给出段错误。6
和7
给出了非法指令。
我收到各种总线错误、段错误和非法指令,直到
const main=194
根本没有打断我(至少没有打断我生成这些小程序的 python 脚本)。
还有一些其他数字也不会导致 exceptions/interrupts,因此不会导致 Unix 信号。我查了一对夫妇的return密码,return密码是252
。我不知道为什么或那意味着什么或它是如何到达那里的。
204
给了我一个 "trace trap"。这是 0xcc
,我知道这是 int3
中断 - 这很有趣! (241/0xf1 也让我得到这个)
无论如何,它一直在继续,显然主要是总线错误和段错误以及一些非法指令,偶尔...做任何事情然后 returns 252...
我在谷歌上搜索了一些操作码,但我真的不知道我在做什么,或者老实说该去哪里寻找。我什至还没有查看我所有的输出,只是一直在滚动浏览。我知道段错误是对有效内存的无效访问,而总线错误是对无效内存的访问,我计划查看数字的模式并找出这些问题发生的位置和原因。但是252的事情让我有点难过。
#!/usr/bin/env python3
import os
import subprocess
import time
import signal
os.mkdir("testc")
try:
os.chdir("testc")
except:
print("Could not change directory, exiting.")
for i in range(0, 65536):
filename = "test" + str(i) + ".c"
f = open(filename, "w")
f.write("const main=" + str(i) + ";")
f.close()
outname = "test" + str(i)
subprocess.Popen(["gcc", filename, "-o", outname], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(1)
err = subprocess.Popen("./" + outname, shell=True)
result = None
while result is None:
result = err.poll()
r = result
if result == -11:
r = "segfault"
if result == -10:
r = "bus error"
if result == -4:
r = "illegal instruction"
if result == -5:
print = "trap"
print("const main=" + str(hex(i)) + " : " + r)
这会在 testc/test20.c
中生成一个 C 程序,如
const int main=20;
然后用gcc
编译并运行。 (并在尝试下一个数字之前休眠 1 秒。)
没有期望。我只是想看看发生了什么。
int main = 194
是 c2 00 00 00
,解码为 ret 0
无论调用什么main
,都必须在RAX的低字节中留下252。 (调用约定说 RAX 是 return-value 寄存器,但它不是 arg-passing 寄存器,所以在函数入口上它包含调用者使用它的任何 tmp 垃圾。)
请参阅答案底部的理论,了解为什么 SIGBUS 代表 2 而 SIGSEGV 代表 3:我认为 RAX 是进入 main 的有效指针(偶然动态链接器在那里的内容),03 00 add eax, [rax]
销毁它但 02 00 add al, [rax]
没有,然后从 main
的下 2 个字节开始在 00 00 add [rax], al
上执行任一错误,或者运行 00 00
指令然后从页面末尾掉下来。
main
(在read-only TEXT段),存储到read-only 页面也有 SIGBUS。 所以如果 RAX 仍然指向那里,00 00 add [rax], al
也会因为这个原因而 SIGBUS。
(请注意,这个答案有一些错误的猜测,并且每次我从@SWilliams 或@MichaelPetch 获得新信息时都没有完全重写。要点是关于哪种 #PF
导致哪个信号上升到目前为止,我已经尝试至少在不太准确的事情之后添加一个更正。我认为错误的理论有一些价值,作为其他类型事情的例证可能 已经发生,所以我将其全部留在此处。)
你的 Python 程序在我的 Linux 机器上失败,一旦它到达 c2 00 00 00
ret imm16
,第一个 return 成功。 (在 Linux 上,.rodata
部分在 之后 .text
in the TEXT segment 结束,所以 main
没有任何东西可以下降入。)
...
const main=0xc0 : segfault
const main=0xc1 : segfault
Traceback (most recent call last):
File "./opcode-test.py", line 34, in <module>
print("const main=" + str(hex(i)) + " : " + r)
TypeError: must be str, not int
难道 python 没有 strsignal(3)
的等价物来将信号映射到标准文本字符串,例如 "Illegal instruction"? (像 strerror
但信号代码而不是 errno 值?)
大多数 x86 指令都是多字节长。 x86 是 little-endian,所以您主要查看
?? 00 00 00 90 90 90 ...
或更大的整数 ?? ?? 00 00 90 90 90 90 ...
,假设您的链接器使用 0x90 nop
填充函数之间的字节,就像 Linux 上的 GNU ld
那样。
这些字节序列可能会在您命中 NOP 之前解码为一条或多条有效指令,并落入链接器在 main
之后放置的任何 CRT 函数。如果你没有错误地到达那里,并且没有偏移堆栈指针,那么你已经进入了堆栈上具有有效 return 地址的函数(main
的调用者,另一个 CRT 函数)就像 main tail-called它。
大概是函数 returns 252(或一些低字节为 252 的更宽的值)。从 main
返回会导致干净的进程退出,使用 main
的 return 值进行退出系统调用。
这个 fall-through 尾调用就像 main 以 return next_function(argc, argv);
结束一样。
更正(没有重写整个答案,抱歉)
因为 main=194
是第一个有效的,我认为你实际上并没有得到 fall-through,可能只有 C2 ret imm16
和 C3 ret
通向干净的出口。对于 c2
,它必须后跟 2 00
个字节,否则它会破坏 main
的调用者的堆栈。
或那些带有不做任何事情的前缀的指令,或无害的one-byte指令。例如90 nop
/ c3 ret
或 90 nop
/ c2 00 00 ret 0
。或者 91 xchg eax, ecx
等实际上可以给你一个不同的 return 值,用另一个寄存器交换 EAX。 (x86 将操作码 90 .. 97
专用于 xchg-with-EAX,因为在原始 8086 AX 上更多 "special",没有像 movsx
到 sign-extend 这样的指令进入其他寄存器。并且没有 2操作数 imul
.
其他无害的one-byte指令包括99 cdq
和98 cwde
,但不包括push
或pop
(因为更改 RSP 会使它不指向 return 地址)。一些 one-byte 标志 set/clear 指令是 f9 stc
、fd std
,但不是 fb sti
(这是特权,与进位标志和方向标志不同)。
无害前缀是 0x40..4f
REX 前缀、0xf2/
f3REP, and
0x66and
0x67` operand-size 和地址大小。此外,任何 segment-override 前缀也可能是无害的。
我刚刚测试了 main=0xc366
和 main=0xc367
是的,它们都干净地退出了。 GDB 将 66 c3
解码为 retw
(operand-size 前缀)并将 67 c3
解码为 addr32 ret
(地址大小前缀),但两者仍弹出 64 位 return 地址,也不要截断堆栈指针。 (我取出了我一直在使用的 -no-pie
,所以 RIP 和 RSP 一起在低 32 位之外)。
注意 00
是 add [r/m8], r8
的操作码,所以 00 00
解码为 add [rax], al
.
要通过那些 00
字节并到达链接器作为填充插入的 "nop sled",您需要操作码(如果操作码使用一个,则需要 modrm 字节)编码较长指令的开始 ,例如 0xb8 mov eax, imm32
,它有 5 个字节长,并消耗 0xb8
之后的下一个 4 个字节。其实还有short-formmov-immediate 每个寄存器的编码,所以 0xb8 + 0..7
都会让你跨越差距。除了mov esp, imm32
,一旦执行到下一个函数就会崩溃,因为它踩到了堆栈指针。
早期的一个是 05
,add eax, imm32
的 short-form(无 modrm)操作码。大多数原始 8086 ALU 指令具有特殊的 AX,imm16 / EAX,imm32 短格式,而不是使用 ModRM 字节编码目标操作数的 op r/m32, imm32
或 imm8
格式。 (并且 ModRM 中 /r
字段的位作为额外的操作码位。)
有关 AL/EAX/RAX 短格式编码和单字节指令的更多信息,请参见Tips for golfing in x86/x64 machine code。
手动解码x86机器码,看Intel的手册,特别是vol.2手册,详细说明了指令编码格式,最后有一个操作码table。 (参见链接 in the x86 tag wiki). For just an opcode map, see http://ref.x86asm.net/coder64.html.
使用反汇编器或调试器查看您的 executables
中有什么但实际上,使用像 objdump -drwC -Mintel
这样的反汇编程序。或者 llvm-objdump
。在输出中找到 main
,然后查看得到的结果。 (或者使用 GDB,因为指令中间的标签会使反汇编器失效。)
使用objdump -rwC -Mintel -D -j .rodata -j .text testc/test194
得到这样的输出,将.text和.rodata部分反汇编为代码:
testc/test194: file format elf64-x86-64
Disassembly of section .text:
0000000000400540 <__libc_csu_init>:
400540: 41 57 push r15
400542: 49 89 d7 mov r15,rdx
...
4005a4: c3 ret
4005a5: 90 nop
4005a6: 66 2e 0f 1f 84 00 00 00 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
00000000004005b0 <__libc_csu_fini>:
4005b0: c3 ret
Disassembly of section .rodata:
00000000004005c0 <_IO_stdin_used>: ;;;; This is actually data!
4005c0: 01 00 add DWORD PTR [rax],eax
4005c2: 02 00 add al,BYTE PTR [rax]
00000000004005c4 <main>:
4005c4: c2 00 00 ret 0x0
... ; objdump elided the last 0, not me. It literally put ...
(我修改了你的 python 脚本以添加 -no-pie
gcc 选项,这就是为什么我的反汇编具有绝对地址,而不仅仅是相对于文件开头的小地址 = 0。我想知道这是否会把 main
放在它可能掉下来的地方,但它没有。)
请注意,.text 和 .rodata 之间只有很小的差距。它们属于同一 ELF 段(在 OS 的程序加载器查看的 ELF 程序头中),因此它们属于同一映射,它们之间没有未映射的页面。 如果幸运的话,中间的字节甚至用 0x90 nop
填充而不是 00
。实际上,有些东西用长 NOP 填补了 __libc_csu_init
和 __libc_csu_fini
之间的空白。如果它们在同一个源文件中,可能是来自汇编程序。
main
当然在 .rodata
中,因为您在 C 中将其声明为 read-only 全局(静态存储),如 const int main = 6;
。我你用了 const int main __attribute__((section(".text"))) = 123
,你可以在正常的 .text
部分得到 main
。在我的系统上,它在 __libc_csu_init
.
但是标签会中断反汇编;反汇编器认为它一定是错误的并从标签重新开始解码。所以在 testc/test5
上的 GDB 中(使用 set disassembly-flavor intel
和 layout reg
,然后使用 start
命令在 main
开始时停止),我会得到
|0x40053c <main> add eax,0x41000000 │
│0x400541 <__libc_csu_init+1> push rdi │
│0x400542 <__libc_csu_init+2> mov r15,rdx
但是从 objdump -drwC -Mintel
开始(仅反汇编 .text
部分是 -d
的默认设置,我使用 GNU C 属性将 main
放在那里所以我的程序可以按照你的方式工作),我得到:
000000000040053c <main>:
40053c: 05 00 00 00 ....
0000000000400540 <__libc_csu_init>:
400540: 41 57 push r15
400542: 49 89 d7 mov r15,rdx
请注意,与 05 00 00 00
位于同一行的 ....
表示解码没有到达指令的末尾。
并且由于 main
在这里没有按 16 对齐,所以它恰好与 __libc_csu_init
的开头对齐。因此 add eax, imm32
从 push r15
消耗 REX.W 前缀(41
),如果通过从 main 掉落而不是通过调用到达,则将其解码为 push rdi
到 __libc_csu_init
标签。
以上输出来自Linux。您的 OS X 系统会有所不同
OS X 将大部分 CRT 启动代码放在 libc 中,而不是静态链接到 executable 和 main
.
或者也许您的主线没有任何东西可以落入
如果有,main=5
会起作用,但你说第一个 non-crashing 结果是 main=194
,这是一个实际的 ret
.
如果在 c3 ret
或 c2 00 00 ret 0
returned 之前没有任何东西,那么在 main
之后可能没有什么可落入的,或者间隙没有用重复的 [ 填充=48=] 形成一个 "nop sled" 如果解码从中间的任何地方开始,它将执行正常。 (例如,在较早的指令消耗了双字 int main
末尾的尾随 0
字节和一些填充字节之后。)
I understand that a segfault is invalid access to valid memory and a bus error is access to invalid memory
不,那个简化的描述是倒退的。通常你会在所有 Unix 上尝试访问未映射的页面时遇到段错误。但是对于某些类型的无效访问(即使在有效地址上),您会收到总线错误。
SPARC 上的 Solaris 为未对齐字 loads/stores 到有效内存提供总线错误。
在 x86-64 Linux 上,您只会在非常奇怪的情况下获得 SIGBUS。参见 Debugging SIGBUS on x86 Linux。 Non-canonical 导致 #SS
异常的堆栈指针,读取超过被截断的 mmap
ed 文件的末尾。此外,如果您启用 x86 对齐检查(AC 标志),但没有人这样做,因为像 memcpy 这样的库函数使用未对齐的 loads/stores,并且编译器 code-gen 假定未对齐的整数 loads/stores 是安全的。
IDK 什么硬件异常 *BSD 映射到 SIGBUS,但我假设常规 out-of-bounds 访问,如 NULL-pointer 取消引用,将 SIGSEGV挺标准的。
@MichaelPetch 在评论中说 on OS X
#PF
(page fault hardware exception) 来自code-fetch 例内核传递SIGBUS#PF
从数据 load/store 到未映射的页面导致 SIGSEGV。#PF
从商店到 read-only 页面导致 SIGBUS。 (并且 this 是02 00 add al, [rax]
之后发生的事情,在00 00 add [rax], al
中形成main
的第二个字节。这个答案的其余部分没有考虑到这一点。)
(当然,这是在检查 page-fault 是否是由于硬件页面 table 和逻辑进程内存映射之间的差异,例如来自惰性映射 copy-on-write,或页面调出到磁盘。)
因此,如果您的 int main
位于未映射页面的末尾,05 add eax,imm32
会在包含 [=132= 的双字的末尾读取一个额外的字节](GAS 语法汇编中的 .long 5
)。 这将进入下一页和 SIGBUS。(您最后的评论表明它确实是 SIGBUS。)
关于前几个值的理论:
您举报:
- main =
02 00 add al, [rax]
/ `00 00 add [rax], al 的总线错误
- 但是 main =
03 00 add eax, [rax]
/00 00 add [rax], al
. 的段错误
我们知道RAX的低字节是252
,所以如果RAX持有一个有效的指针值,它就是4字节对齐的。因此,如果从 [rax]
加载一个字节有效,那么加载一个双字也是如此。
所以很可能memory-sourceadd
是成功的,并且修改了AL,RAX的低字节(字节操作数大小)大概还是留给RAX一个有效指针。** 然后,如果包含 main
的页面的其余部分充满了 00 00 add [rax], al
指令(或者只是 main 本身内部的指令),这些指令将成功(无需进一步修改 RAX),直到执行失败进入未映射的页面,只要 RAX 仍然 是 运行 之后的有效指针,无论 main
解码为什么。
实际上,memory-destination add
本身出现故障并引发 SIGBUS。
03 00 add eax, [rax]
写入 EAX,从而将 RAX 截断为 32 位。(将 32 位寄存器隐式 zero-extends 写入完整的 64-位寄存器,不像写低 8 位或 16 位部分寄存器。)这肯定会给你一个无效的指针,因为 OS X 映射静态 code/data 在低 32 位之外虚拟地址 space.
所以 下面的 00 00 add [rax], al
肯定会因尝试写入 out-of-bounds 地址而导致 出错,导致引发 SIGSEGV 的 #PF
。
可能只是一页结束前main
的最后两个字节00 00
00 00
。 否则 05 add eax, imm32
会因截断 RAX 而发生段错误,然后 运行 00 00 add [rax], al
。对于 SIGBUS,它必须 code-fetch 进入一个未映射的页面,之后不解码任何 memory-access 指令。
对于您所看到的肯定还有其他合理的解释,但我认为这可以解释您到目前为止的所有观察结果;没有更多数据,我们无法反驳它。显然,最简单的方法是启动 GDB 或任何其他调试器,只需 start
/ si
并观察会发生什么。