如果函数没有明确使用'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',为啥没有返回值的主要内容,如果未能解决你的问题,请参考以下文章

递归幂函数:如果没有初始返回值,为啥会这样?

函数返回值

当一个函数无返回值时,函数的类型应定义为啥

为啥在 Laravel 中使用 PHPUnit 的函数“link_to”没有返回值?

复习1

unity中协程函数没有错误为啥会报错