main関数の戻り値について

を見て、どのレベルでその設定が行われているんだろうって思って
調べてみた。

環境

Ubuntu 10.10 x64 GCC-4.4.5

サンプルプログラム

id:n7shi さんと同じものを使います。以下ではファイル名は sample.cとします。

int main(void)
{
}

c99の場合

c99では 0になるようなので、先にそちらから確認します。
以下のコマンドでコンパイルを行います。

  % gcc -g -std=c99 -c sample.c 

得られたオブジェクトファイルを逆アセンブルします。

  % objdump -d sample.o L 
  sample.o:     file format elf64-x86-64

  Disassembly of section .text:

  0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   b8 00 00 00 00          mov    $0x0,%eax
   9:   c9                      leaveq 
   a:   c3                      retq   

中身は何もない mainですが, eaxレジスタに 0が格納されています。
eaxレジスタは関数の戻り値を格納するレジスタです.(Calling Conventionが
cdeclの場合. Linux GCCでは何も指定しなければ cdeclである)


'return 0'と明示的に書かなくても仕様に準拠するために、
コンパイラがそのコードを挿入してくれることがわかります。

c89(ANSI C)

一方 c89で同様のことをしてみます。コンパイルは以下のコマンドになります。

  % gcc -g -std=c89 -c sample.c 


アセンブルは同じコマンドになります。

  % objdump -d sample.o
  sample.o:     file format elf64-x86-64

  Disassembly of section .text:

  0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   c9                      leaveq 
   5:   c3                      retq   

eaxレジスタには何も設定していません。入力したコードの通り
戻り値が何もないということがわかります。

補足

厳密にいうと $?に設定される値は main関数の戻り値ではなく、
_exit or exit_groupシステムコールの引数になります。


そのあたりを確認してみましょう。
以下のコマンドでコンパイルします。

  % gcc -g -std=c99 -static sample.c

'-static'とすることが重要です。初期設定をして main関数を呼び出したり、
exitするオブジェクトファイルを静的にリンクしてしまいます。


GDBを起動し, _exitにbreakpointを設定します。ここでいう _exitは
libcの関数のものを示します。そして _exitまで走らせます。

  % gdb a.out
  (gdb) break _exit
  (gdb) run

どのような命令列かを確認します。システムコールを発行する命令は
syscallになります. また x86_64ではシステムコール番号が %eax,
第一引数が %rdiレジスタに格納されます.

(gdb) x/20i $pc
=> 0x40d310 <_exit>:	movslq %edi,%rdx
   0x40d313 <_exit+3>:	mov    $0xffffffffffffffd0,%r9
   0x40d31a <_exit+10>:	mov    $0xe7,%r8d
   0x40d320 <_exit+16>:	mov    $0x3c,%esi
   0x40d325 <_exit+21>:	jmp    0x40d340 <_exit+48>
   0x40d327 <_exit+23>:	jmp    0x40d330 <_exit+32>
   0x40d329 <_exit+25>:	nop
   0x40d32a <_exit+26>:	nop
   0x40d32b <_exit+27>:	nop
   0x40d32c <_exit+28>:	nop
   0x40d32d <_exit+29>:	nop
   0x40d32e <_exit+30>:	nop
   0x40d32f <_exit+31>:	nop
   0x40d330 <_exit+32>:	mov    %rdx,%rdi
   0x40d333 <_exit+35>:	mov    %esi,%eax
   0x40d335 <_exit+37>:	syscall 
   0x40d337 <_exit+39>:	cmp    $0xfffffffffffff000,%rax
   0x40d33d <_exit+45>:	ja     0x40d358 <_exit+72>
   0x40d33f <_exit+47>:	hlt    
   0x40d340 <_exit+48>:	mov    %rdx,%rdi

syscall命令までプログラムカウンタを進めます。進めすぎないように
ステップ毎に命令を表示するようにします。なおステートメントレベル
ではなく、命令レベルでステップ実行をする場合は 'step(s)'では
なく 'stepi(si)'を使います。

  (gdb) display/i $pc
  (gdb) # syscall命令まで stepi(or si). 到達すれば以下が表示されるはず
   => 0x40d346 <_exit+54>:	syscall 

ここでレジスタを %raxと %rdiレジスタの値を確認します.

  (gdb) printf "rax=%d, rdi=%x\n", $rax, $rdi
   rax=231, rdi=0

となりました. x86_64でシステムコール番号 231は exit_groupです.
(システムコール番号は /usr/include/asm/unistd_64.hで確認できます。)
また rdiが 0なので、$?の値が 0になることがわかります。

まとめ

C99で mainの戻り値が 0になるのにどういうコードをコンパイラ
挿入しているかがわかりました。メジャーなコンパイラは c99に
対応してますが、組み込みではまだまだ対応していないコンパイラ
多いので、main関数が正常に終了するという場合については明示的に
'return 0;'と書いた方がいいでしょう。