如果函数没有明确使用'ret',为啥没有返回值
Posted
技术标签:
【中文标题】如果函数没有明确使用\'ret\',为啥没有返回值【英文标题】:Why is no value returned if a function does not explicity use 'ret'如果函数没有明确使用'ret',为什么没有返回值 【发布时间】:2013-12-14 00:10:56 【问题描述】:我有以下程序:
SECTION .text
main:
mov ebx, 10
mov ecx, 50
repeat:
inc ebx
loop repeat
mov eax, ebx
ret
当这个程序运行时,它按预期返回 60。但是,如果您删除最后的 ret
语句,程序运行正常,但随后返回 0。这是为什么呢?
【问题讨论】:
你怎么知道它“运行良好”? 【参考方案1】:当您离开“ret”时,计算机会执行最后一个“move eax, ebx”,然后执行计算机内存中发生的任何事情。
我很惊讶您没有收到非法指令/访问;这将是最常见的反应。不知何故,垃圾指令在破坏寄存器之后就像返回一样。
“返回 60”是什么意思也有点不清楚。你的意思是作为命令提示符的值?很明显,您的程序无法防御非法指令陷阱。 我不清楚当你遇到这样一个没有防御的陷阱时 Windows 会做什么。我从经验中知道,当我这样做时,Windows 往往会终止我的进程,并且我会得到一些随机退出状态。 “0”可能就是这样的状态。
尝试添加:
mov byte ptr[eax], 0
在“ret”指令之前;这将导致非法的内存引用。你报告你得到什么状态。如果您在这种情况下得到零状态结果,我不会感到惊讶。
【讨论】:
OP 显然很幸运,链接器在main
之后放置的任何函数都返回零。 main
进入代码 (.text
) 段,以及通过与标准运行时链接获得的运行时样板启动函数。我希望另一个函数会跟随它,而不是数据。请记住这是main
,而不是_start
。
很明显他的意思是main的返回值,其中的低8位最终作为Unix进程退出状态。既然您问了,以SIGSEGV
终止的Unix 进程的退出状态为139。(分段违规=非法内存访问)。 SIGSEGV = 信号 11,并且 139 = 128+11。 ***.com/a/1104641/224132。希望这有助于解释当他让 main 失败时 OP 看到了什么。【参考方案2】:
因为它通过并运行链接器放在它之后的下一个函数。
请参阅我关于 Ira 的答案的 cmets,了解为什么您的代码不会崩溃。如果您没有与 C 运行时库启动代码链接(即您只有 _start
而不是 main
),则执行将遇到一些非代码,并且非法指令出错,或者尝试访问未映射的内存。见下文。
反汇编你的最终二进制文件,看看发生了什么。当我尝试这个时,我发现链接器将main
放在标准C 运行时启动函数frame_dummy
和__libc_csu_init
之间。它
00000000004004f6 <main>:
4004f6: b8 0a 00 00 00 mov $0xa,%eax
4004fb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
0000000000400500 <__libc_csu_init>:
400500: 41 57 push %r15
400502: 41 56 push %r14
400504: 41 89 ff mov %edi,%r15d
400507: 41 55 push %r13
... a bunch more code that eventually returns.
您可能已经发现使用调试器、单步指令会发生什么。
顺便说一句,如果您确实使用gcc -static -nostartfiles
或通过自己组装(as foo.s
)/链接(ld foo.o
)制作了一个独立的二进制文件,您将获得一个包含您的一条指令的 888 字节文件,其中剩下的是 ELF 标头和东西。
$ cat > fallthrough.s <<EOF
.globl main
main:
.globl _start
_start:
mov $10, %eax
# fall through
EOF
$ gcc -g -static -nostartfiles fallthrough.s -o fallthrough
$ gdb fallthrough
(gdb) b _start # breakpoint
(gdb) r # run the prog
(gdb) disassemble /r _start, _start+40
Dump of assembler code from 0x4000d4 to 0x4000fc:
=> 0x00000000004000d4 <main+0>: b8 0a 00 00 00 mov $0xa,%eax
0x00000000004000d9: 00 00 add %al,(%rax)
0x00000000004000db: 00 00 add %al,(%rax)
0x00000000004000dd: 00 00 add %al,(%rax)
0x00000000004000df: 00 2c 00 add %ch,(%rax,%rax,1)
0x00000000004000e2: 00 00 add %al,(%rax)
0x00000000004000e4: 02 00 add (%rax),%al
0x00000000004000e6: 00 00 add %al,(%rax)
0x00000000004000e8: 00 00 add %al,(%rax)
0x00000000004000ea: 08 00 or %al,(%rax)
0x00000000004000ec: 00 00 add %al,(%rax)
0x00000000004000ee: 00 00 add %al,(%rax)
0x00000000004000f0: d4 (bad)
0x00000000004000f1: 00 40 00 add %al,0x0(%rax)
...
(gdb) layout asm #text-window mode. layout reg is great for single-stepping, BTW.
(gdb) si # step instruction
0x00000000004000d9 in ?? ()
(gdb) si
Program received signal SIGSEGV, Segmentation fault.
0x00000000004000d9 in ?? ()
(gdb) c
Continuing.
Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
内存中代码后面的00
字节也存在于 ELF 可执行文件中。文件的内存映射仅发生在页面粒度上,因此最终都映射为可执行指令。 (机器代码不会从可执行文件的磁盘缓存中复制出来;内存只是以读取+执行权限映射到execve(2)
s 二进制文件的进程中。)
$ objdump -s a.out
a.out: file format elf64-x86-64
Contents of section .note.gnu.build-id:
4000b0 04000000 14000000 03000000 474e5500 ............GNU.
4000c0 db31c97d 55481b9a 57110753 1786dd1a .1.UH..W..S....
4000d0 11679958 .g.X
Contents of section .text:
4000d4 b80a0000 00 .....
Contents of section .debug_aranges:
0000 2c000000 02000000 00000800 00000000 ,...............
0010 d4004000 00000000 05000000 00000000 ..@.............
0020 00000000 00000000 00000000 00000000 ................
...
$ size a.out
text data bss dec hex filename
41 0 0 41 29 a.out
剥离二进制文件仍然会导致段错误,但使用不同的指令。好吗?
# b _start would be b *0x4000d4 without symbols.
(gdb) r
...
Program received signal SIGSEGV, Segmentation fault.
0x00000000004000d9 in ?? ()
(gdb) disassemble /r $rip-5, $rip +15
Dump of assembler code from 0x4000d4 to 0x4000e8:
0x00000000004000d4: b8 0a 00 00 00 mov $0xa,%eax
=> 0x00000000004000d9: 00 2e add %ch,(%rsi)
0x00000000004000db: 73 68 jae 0x400145
0x00000000004000dd: 73 74 jae 0x400153
0x00000000004000df: 72 74 jb 0x400155
0x00000000004000e1: 61 (bad)
0x00000000004000e2: 62 (bad)
0x00000000004000e3: 00 2e add %ch,(%rsi)
0x00000000004000e5: 6e outsb %ds:(%rsi),(%dx)
0x00000000004000e6: 6f outsl %ds:(%rsi),(%dx)
0x00000000004000e7: 74 65 je 0x40014e
$ hexdump -C a.out
...
000000d0 11 67 99 58 b8 0a 00 00 00 00 2e 73 68 73 74 72 |.g.X.......shstr|
000000e0 74 61 62 00 2e 6e 6f 74 65 2e 67 6e 75 2e 62 75 |tab..note.gnu.bu|
000000f0 69 6c 64 2d 69 64 00 2e 74 65 78 74 00 00 00 00 |ild-id..text....|
00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
我们的 mov
指令是我从 hexdump 中包含的第一行中的 b8 0a 00 00 00
。我认为下面的00 2e ...
是一个 ELF 数据结构,可能是部分的索引或其他东西。作为 x86 指令,它是一个 add %ch,(%rsi)
,它会出现段错误,因为 %rsi
没有指向可写内存。 (ABI 说除了堆栈指针之外的寄存器在进程入口时未定义,但 Linux 选择在 ELF 加载器中将它们归零以避免泄漏内核数据。%rsi
不指向可写内存,进程可能不指向有。)
那么,如果您在此处添加退货呢?不,没有什么可以返回到。堆栈包含指向进程 args 环境变量的指针。您必须进行exit
系统调用。
.section .text
.globl _start
_start:
xor %edi, %edi
mov $231, %eax # exit(0)
syscall
# movl $1, %eax # The 32bit ABI works even for processes in long mode, BTW.
# int $0x80 # exit(edx)
【讨论】:
哇,我写这篇文章的时间比我打算的要长得多。 >. "那么执行会命中一个"命中一个什么? ;) @Michael:谢谢。我刚刚结束了我的漫无边际,并没有真正校对后意识到我可能已经写了一个多小时,关于一个愚蠢的老问题。以上是关于如果函数没有明确使用'ret',为啥没有返回值的主要内容,如果未能解决你的问题,请参考以下文章