main にたどり着く前を追ってみた

結婚式準備の気分転換とgdbの練習を兼ねてmainにたどり着くまでの動きを追った。 GCCでは以下の関数を設定するとmainに入る前に実行される。

1
2
3
__attribute__((constructor)) void constructor(){
  printf("constructor\n");
}

Linux環境のバイナリはELF形式が一般的で、ELFでは_startデフォルトのエントリポイントになる

今回の整理

だいたい下の事がわかったが、main の中でバックトレースを確認しても_start などが表示されない理由がわからなかった。

  • GCCを使うとELFのデフォルトエントリポイントは_start
  • _start/usr/lib64/crt1.o由来
  • OSXはELFファイルでなく_startがリンク時に衝突する事はない
  • GCC拡張のconstructor__libc_csu_init()から呼ばれる
  • mainは__libc_start_main()から呼ばれる
  • ELFのエントリポイントは変更可能だが後処理の自作が必要になる

一応main 外部がバックトレースで表示されない理由は下が考えられる。

  • スタック積み上げ動作が異なり呼び元の情報が不完全になる
  • gdbが気を効かせてmain以降を遡らない

とりあえず切り分けるには手作業でESP, EBPを追いmainから__libc_csu_init,_startに辿り着けるか確認すれば良い。 そしてgdbの優しさの疑いが高まった場合に、ソースコードを読みに行けば解決しそう。 ヘタレとしては面倒くさくなりそうな気がする。

サンプルを準備する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>

__attribute__((constructor)) void constructor(){
  printf("constructor\n");
}

int main() {
  int idx = 10;
  printf("test:idx:%d \n", idx);
  return 0;
}
1
$ gcc -gdwarf-2 -gstrict-dwarf -g -O0 ./sample.c

_startがリンクされる事を確認する

バイナリに含まれる事を確認する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ objdump -D -M intel ./a.out | grep -A 15 '<_start>'
0000000000400490 <_start>:
  400490:       31 ed                   xor    ebp,ebp
  400492:       49 89 d1                mov    r9,rdx
  400495:       5e                      pop    rsi
  400496:       48 89 e2                mov    rdx,rsp
  400499:       48 83 e4 f0             and    rsp,0xfffffffffffffff0
  40049d:       50                      push   rax
  40049e:       54                      push   rsp
  40049f:       49 c7 c0 30 06 40 00    mov    r8,0x400630
  4004a6:       48 c7 c1 c0 05 40 00    mov    rcx,0x4005c0
  4004ad:       48 c7 c7 90 05 40 00    mov    rdi,0x400590
  4004b4:       e8 b7 ff ff ff          call   400470 <__libc_start_main@plt>
  4004b9:       f4                      hlt
  4004ba:       66 90                   xchg   ax,ax
  4004bc:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]

_start は /usr/lib64/crt1.o 由来

以下の関数を定義して衝突させる。

1
void _start() {}

以下の様に衝突させて衝突先オブジェクトファイルを確認した。ちなみにOSXでは衝突しない。

1
2
3
4
5
$ gcc -O0 ./sample.c
/tmp/ccmwPDqJ.o: In function `_start':
sample.c:(.text+0x10): multiple definition of `_start'
/usr/lib/gcc/x86_64-redhat-linux/4.8.3/../../../../lib64/crt1.o:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

constructorは__libc_csu_init から呼ばれる

以下の様に呼び元を確認できる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ gdb ./a.out
(gdb) b constructor
Breakpoint 1 at 0x400584: file ./sample.c, line 4.
(gdb) r
Starting program: /home/vagrant/works/playground/./a.out

Breakpoint 1, constructor () at ./sample.c:4
(gdb) bt
#0  constructor () at ./sample.c:4
#1  0x000000000040060d in __libc_csu_init ()
#2  0x00007ffff7a3ba85 in __libc_start_main () from /lib64/libc.so.6
#3  0x00000000004004b9 in _start ()

main は __libc_start_main の中で呼ばれる

mainを読んでいたのは__libc_start_mainの以下にあった。$raxmainである事は確認した。 下の箇所のcallmainが実行されるのは確認できたが、mainの中からバックトレースで_start が見えない事が疑問として残った。jmpじゃない。

(gdb) disassemble __libc_start_main
//...
   0x00007ffff7a3baaf <+175>:   jne    0x7ffff7a3bb03 <__libc_start_main+259>
   0x00007ffff7a3bab1 <+177>:   mov    %fs:0x300,%rax
   0x00007ffff7a3baba <+186>:   mov    %rax,0x68(%rsp)
   0x00007ffff7a3babf <+191>:   mov    %fs:0x2f8,%rax
   0x00007ffff7a3bac8 <+200>:   mov    %rax,0x70(%rsp)
   0x00007ffff7a3bacd <+205>:   lea    0x20(%rsp),%rax
   0x00007ffff7a3bad2 <+210>:   mov    %rax,%fs:0x300
   0x00007ffff7a3badb <+219>:   mov    0x3983be(%rip),%rax        # 0x7ffff7dd3ea0
   0x00007ffff7a3bae2 <+226>:   mov    0x8(%rsp),%rsi
   0x00007ffff7a3bae7 <+231>:   mov    0x14(%rsp),%edi
   0x00007ffff7a3baeb <+235>:   mov    (%rax),%rdx
   0x00007ffff7a3baee <+238>:   mov    0x18(%rsp),%rax
=> 0x00007ffff7a3baf3 <+243>:   callq  * %rax
   0x00007ffff7a3baf5 <+245>:   mov    %eax,%edi

また__libc_start_mainは動的に作られるかロード時に再配置されておりobjdumpと異なる命令列がメモリ上には存在した。 シンボルがPLT上に居るので関係していそう。

1
2
3
4
5
$ objdump -D -M intel ./a.out  | grep -A 4 '<__libc_start_main@plt>:'
0000000000400470 <__libc_start_main@plt>:
  400470:       ff 25 b2 0b 20 00       jmp    QWORD PTR [rip+0x200bb2]        # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
  400476:       68 02 00 00 00          push   0x2
  40047b:       e9 c0 ff ff ff          jmp    400440 <_init+0x28>

エントリポイントを素朴に変更してもセグフォる

以下の様にエントリポイントを変更しても後処理が上手くいかないのかmain実行後にセグメンテーションフォルトを起こす。 ちなみに_startを経由せずmainに入る事はgdbで確認できる。

1
2
3
4
$ gcc -e main -gdwarf-2 -gstrict-dwarf -g -O0 ./sample.c
[vagrant@pit(10.0.2.2) playground]$ ./a.out
test:idx:10
Segmentation fault
comments powered by Disqus