如果函数没有明确使用'ret',为什么没有返回值

时间:2013-12-14 00:10:56

标签: assembly x86

我有以下程序:

SECTION .text
main:
     mov ebx, 10
     mov ecx, 50

repeat:
     inc ebx
     loop repeat

     mov eax, ebx
     ret

当该程序运行时,它会按预期返回60。但是,如果删除最后的ret语句,程序运行正常,但随后返回0.为什么会这样?

2 个答案:

答案 0 :(得分:6)

当您退出“ret”时,计算机执行最后一次“move eax,ebx”,然后执行计算机内存中接下来发生的任何事情。

我很惊讶你没有得到非法指令/访问;这将是最常见的回应。在捣毁寄存器之后,垃圾指令就像回归一样。

它还有点不清楚你的意思是“返回60”。你的意思是作为命令提示符的值?很明显,你的程序无法防范非法指令陷阱。 当你在没有防御的情况下获得这样的陷阱时,Windows所做的事情对我来说并不清楚;我从经验中知道,当我这样做时,Windows往往只是终止我的进程,我得到一些随机的退出状态。 “0”可能是这样的状态。

尝试添加:

      mov   byte ptr[eax], 0
在“ret”指令之前;这将导致非法的内存引用。您报告您获得的状态。如果你在这种情况下获得零状态结果,那就不会让我感到惊讶。

答案 1 :(得分:2)

因为它会通过并运行链接器后面的下一个函数。

请参阅我对Ira的答案的评论,为什么你的代码不会崩溃。如果您没有与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可执行文件中。文件的内存映射只发生在页面粒度上,因此它最终都被映射为可执行指令。 (机器代码不会从磁盘高速缓存中复制出来用于可执行文件;内存只是将read + execute权限映射到execve(2)二进制文件的进程中。)

$ 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)