はじめに
main関数の前にはスタートアップ関数という物があり、スタートアップ関数ではアプリケーションの実行のための準備がされている。
かといって、スタートアップ関数は特別なプログラムではなく、実際にアプリケーションプログラマが自作することも可能である。
しかし、スタートアップ関数では決まった処理をすることが多く、手間がかかるプログラムなので、ライブラリ提供者がライブラリとして配布してくれている。
今回はmain関数のアセンブラを覗くことでargc・argv・envpの扱いについて見ていこうと思う。
参考程度に次の記事も参照してほしい。
fromnand.hatenadiary.jp
システムがスタートアップ関数を呼び出すまでの流れ
さて、まずプログラムがどのように実行開始するのかを見てみよう。
多くの場合はOSがfork系システムコール→exec系システムコールという処理を行うことでプログラムの実行を開始すると思う。
このexec系システムコールの内部でargc・argv・envpがスタックに積まれたあと、_startといったスタートアップ関数に処理が渡される。
これらの一連の処理の中で、レジスタの設定やメモリ空間の割当など、重要な準備が数多く行われている。
スタートアップ関数がmain関数を呼び出すまでの流れ
argv・argc・envpはシステム側によって用意されるが、それをmain関数に引き渡すのはスタートアップ関数の役目だ。
今回は次のようなスタートアップ関数を作成した。
長いプログラムに見えるが、argc・argv・envpについて関連している部分のみを抜粋すると、注目するべき点はそんなに多くはない。
.code32 .extern main .extern _sctors, _ectors, _sdtors, _edtors .text _start: # この関数は常にファイルの最初に書かなければならない (正確には.textのオフセット0に置かねばならない) xorl %ebp, %ebp # ebpをクリアしておく call _constructors # コンストラクタたちを呼び出す処理 (ただ、引数がvoidの関数しか呼べない...) popl %eax # スタックの頭にはargcが入っている movl %esp, %ecx # その次にはargv[0], argv[1]...が入っている leal 4(%ecx, %eax, 4), %edx # argv + argc * (4 + 1)番地から環境変数のリストが入っている andl $0xfffffff0, %esp # アライメントを16byteの境界に揃える pushl %edx # &envp[0]を積む pushl %ecx # &argv[0]を積む pushl %eax # argcを積む call main # mainを呼び出す pushl %eax # mainからの戻り値をセーブする (eax, ecx, edxは関数呼び出しで書き換えられる可能性があるため) call _destructors # デストラクタたちを呼び出す処理 (コンストラクタ同様に引数がvoidの関数しか呼べないが...) popl %eax # mainからの戻り値をポップする movl %eax, %ebx # mainの戻り値をexitの引数に取る (こいつがシステムへの返り値になる。 echo $? で表示できる。) movl $1, %eax # exitのシステムコール番号 int $0x80 # exitの呼び出し (これがないとプログラムが適切に終了しない) hlt # ここは訪れないはず... _constructors: movl $_sctors, %eax movl $_ectors, %ecx _constructors_loop: cmpl %eax, %ecx jbe _constructors_end pushl %eax pushl %ecx call *(%eax) popl %ecx popl %eax addl $4, %eax jmp _constructors_loop _constructors_end: ret _destructors: movl $_sdtors, %eax movl $_edtors, %ecx _destructors_loop: cmpl %eax, %ecx jbe _destructors_end pushl %eax pushl %ecx call *(%eax) popl %ecx popl %eax addl $4, %eax jmp _destructors_loop _destructors_end: ret
まず、注目するべきなのは次の三行だ。
popl %eax # スタックの頭にはargcが入っている movl %esp, %ecx # その次にはargv[0], argv[1]...が入っている leal 4(%ecx, %eax, 4), %edx # argv + argc * (4 + 1)番地から環境変数のリストが入っている
実はシステムからスタートアップ関数に処理を移す際、スタックのトップにはargcが、そしてその直後にargvが,、さらにその直後にはenvpが積まれている。
argvやenvpは「char**」なので、「char*」の配列としてスタックに積まれている配列の先頭要素を指すポインタ変数(4byte)である。
上記の三行では、スタックのトップに積まれているargcを「popl %eax」で受け取ったあと、その直後のargvの先頭アドレスも「movl %esp, %ecx」で受け取っている。
最後の行では環境変数もedxに受け取るようにしてある。
そして、次に注目すべきなのは次の四行である。
この四行では、argv・argc・envpをスタックに積んで「call main」とすることでmain関数にargv・argc・envpを渡している
main関数が呼び出されたので、これでスタートアップ関数の役割は終了となる。(実際の環境では_startのあとに_libc_start_mainなどが呼ばれていたりもするが...)
pushl %edx # &envp[0]を積む pushl %ecx # &argv[0]を積む pushl %eax # argcを積む call main # mainを呼び出す
main関数でargc・argv・envpを受け取って利用してみる
次はmain関数で実際にargc・argv・envpを利用してみよう。
今回は次のようなプログラムを使用した。
#include <stdio.h> int main(int argc, char *argv[], char *envp[]){ int i = 0; /* argc の確認 */ printf("argc = %d\n", argc); /* argv の確認 */ printf("argv = ["); for (i = 0; i < argc; i++) { printf("\'%s\' ", argv[i]); } printf("]\n"); /* envp の確認 */ printf("envp = ["); for (i = 0; envp[i]; i++) { printf("\'%s\' ", envp[i]); } printf("]\n"); return 0; }
このプログラムに次のようにコマンドラインを与えると、
./test abc def ghi
実行結果はこの様になる。
確かにうまく3つの引数を受け取っているようだ。
shiguren@ubuntu:~/デスクトップ/TEST$ ./arg abc def ghi argc = 4 argv = ['./arg' 'abc' 'def' 'ghi' ] envp = ['CLUTTER_IM_MODULE=xim' 'LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40 ・・・・・・長過ぎるので省略 ]
main関数を逆アセンブルして、argc・argv・envpの部分を解析する
最後に先程のCプログラムを逆アセンブルしてみる。
至るところにコメントを書いているので、それを参考に解析してもらいたい。(最後の方はすこしずつ雑になっているけれど...)
# 呼び出し規約はcdecl 000054d <main>: # 今、espはmainからのリターンアドレスが格納されているアドレスを指している。 # esp+4から上のアドレスにはargc・argv・envpが順にpushされている # esp+4の値をecxに格納。 54d: 8d 4c 24 04 lea 0x4(%esp),%ecx # アライメントを16byteの境界に合わせている? 551: 83 e4 f0 and $0xfffffff0,%esp # アライメントを調節して、今のespがリターンアドレスを指しているのかが定かではなくなったので # リターンアドレスを今のスタックの先頭にpush(コピーといったほうがいい?)する 554: ff 71 fc pushl -0x4(%ecx) # 新しいスタックフレームを作成する常套処理 557: 55 push %ebp 558: 89 e5 mov %esp,%ebp # eax、ecx、edx以外のレジスタを関数内で変更する場合は # 関数のはじめにpushしておく 55a: 56 push %esi 55b: 53 push %ebx 55c: 51 push %ecx # ローカル変数の領域をまとめて確保 55d: 83 ec 1c sub $0x1c,%esp # x86(32bit)におけるPIC(Position Independent Code)を実現するための関数らしい 560: e8 eb fe ff ff call 450 <__x86.get_pc_thunk.bx> 565: 81 c3 6f 1a 00 00 add $0x1a6f,%ebx # ecxは今、argc・argv・envpの先頭アドレスを指している # その値をesiにもコピー 56b: 89 ce mov %ecx,%esi # i = 0 (iのアドレスはebp-0x1c) 56d: c7 45 e4 00 00 00 00 movl $0x0,-0x1c(%ebp) # なんかよくわからん 574: 83 ec 08 sub $0x8,%esp # esiは今、argcの格納されているアドレスを指している # push argc 577: ff 36 pushl (%esi) # ebx-0x18f4の部分は「argc = %d\n」という文字列のアドレス # そのアドレスをeaxに格納 579: 8d 83 0c e7 ff ff lea -0x18f4(%ebx),%eax # 文字列のアドレスをpushして、printfを呼び出す 57f: 50 push %eax 580: e8 4b fe ff ff call 3d0 <printf@plt> # cdeclの規約に従い、関数呼び出し後にespの値を調節する 585: 83 c4 10 add $0x10,%esp 588: 83 ec 0c sub $0xc,%esp # ebx-0x18e9には「argv = [」の文字列のアドレス 58b: 8d 83 17 e7 ff ff lea -0x18e9(%ebx),%eax # 文字列のアドレスをpushしたあと、printfの呼び出し 591: 50 push %eax 592: e8 39 fe ff ff call 3d0 <printf@plt> # cdeclの規約に従い、espの値を調節する 597: 83 c4 10 add $0x10,%esp # ebp-0x1c、つまりiに0を代入する # これは「for(i = 0; i < argc; i++)」の中の「i = 0」の部分 59a: c7 45 e4 00 00 00 00 movl $0x0,-0x1c(%ebp) # for文の内部に侵入する際の常套処理 # 5cb番地にジャンプする 5a1: eb 28 jmp 5cb <main+0x7e> # もし「i < argc」ならば、この部分の処理を実行する # ebp-0x1c、つまりiの値をeaxにコピーする 5a3: 8b 45 e4 mov -0x1c(%ebp),%eax # argvのデータ型は「char**」でサイズは4byte # この部分はargvの先頭からのアドレスのオフセットを計算している # 意味はedxにeax * 4の値をコピー # eaxにはiの値が入っているので、このedxにargvの先頭アドレスを足せば「&argv[i]」の意味になる 5a6: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx # esiはargc・argv・envpの先頭アドレスを指していて # argcのサイズは4byteなので、esi+4はargvを指している # よって、このコードはeaxにargvの先頭アドレスをコピー 5ad: 8b 46 04 mov 0x4(%esi),%eax # edxにオフセット、eaxはargvの先頭アドレス # つまり、argv[i]のアドレスをeaxに格納 5b0: 01 d0 add %edx,%eax # argv[i]の値をeaxにコピー 5b2: 8b 00 mov (%eax),%eax # 毎度、よくわからない 5b4: 83 ec 08 sub $0x8,%esp # argv[i]の値をpush 5b7: 50 push %eax # 「"\'%s\' "」という文字列をpushし、printfを呼び出す 5b8: 8d 83 20 e7 ff ff lea -0x18e0(%ebx),%eax 5be: 50 push %eax 5bf: e8 0c fe ff ff call 3d0 <printf@plt> 5c4: 83 c4 10 add $0x10,%esp # i++ 5c7: 83 45 e4 01 addl $0x1,-0x1c(%ebp) # ebp-0x1c、つまりiの値をeaxにコピー 5cb: 8b 45 e4 mov -0x1c(%ebp),%eax # esiは今、argcのアドレスを指しているので # これは「cmp argc, i」の意味 5ce: 3b 06 cmp (%esi),%eax # jlは符号ありジャンプ命令の一つ。 # この場合、右側の値(eax, i)が左の値((esi), argc)よりも小さい場合にジャンプ # とどのつまり、「for(i = 0; i < argc; i++)」の「i < argc」の部分の処理 5d0: 7c d1 jl 5a3 <main+0x56> # なんだかよくわからない 5d2: 83 ec 0c sub $0xc,%esp # 「]\n」という文字列のアドレスをeaxにコピーしてpush 5d5: 8d 83 26 e7 ff ff lea -0x18da(%ebx),%eax 5db: 50 push %eax # printfの呼び出し 5dc: e8 ff fd ff ff call 3e0 <puts@plt> 5e1: 83 c4 10 add $0x10,%esp 5e4: 83 ec 0c sub $0xc,%esp # 「envp = [」という文字列のアドレスをeaxにコピーしてpush 5e7: 8d 83 28 e7 ff ff lea -0x18d8(%ebx),%eax 5ed: 50 push %eax # printfの呼び出し 5ee: e8 dd fd ff ff call 3d0 <printf@plt> 5f3: 83 c4 10 add $0x10,%esp # i = 0 5f6: c7 45 e4 00 00 00 00 movl $0x0,-0x1c(%ebp) # for文の内部に侵入する 5fd: eb 28 jmp 627 <main+0xda> # 5ff: 8b 45 e4 mov -0x1c(%ebp),%eax 602: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx 609: 8b 46 08 mov 0x8(%esi),%eax 60c: 01 d0 add %edx,%eax 60e: 8b 00 mov (%eax),%eax 610: 83 ec 08 sub $0x8,%esp 613: 50 push %eax 614: 8d 83 20 e7 ff ff lea -0x18e0(%ebx),%eax 61a: 50 push %eax 61b: e8 b0 fd ff ff call 3d0 <printf@plt> 620: 83 c4 10 add $0x10,%esp 623: 83 45 e4 01 addl $0x1,-0x1c(%ebp) # eaxにiの値をコピー 627: 8b 45 e4 mov -0x1c(%ebp),%eax # envpの型は「char**」なので、envpの指す型のサイズは「sizeof(char*) = 4」 # envpのオフセットにはi * sizeof(char*)を使用するので、それをedxにコピー 62a: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx # envpのアドレスをeaxにコピー 631: 8b 46 08 mov 0x8(%esi),%eax # envp + i * sizeof(char*) 634: 01 d0 add %edx,%eax # envp[i]をeaxにコピー 636: 8b 00 mov (%eax),%eax # envp[i]が0であるかどうかの判定 638: 85 c0 test %eax,%eax # 0以外だった場合は5ff番地にジャンプ 63a: 75 c3 jne 5ff <main+0xb2> # よくわからない 63c: 83 ec 0c sub $0xc,%esp # 「]\n」という文字列のアドレスをpush 63f: 8d 83 26 e7 ff ff lea -0x18da(%ebx),%eax 645: 50 push %eax # putsの呼び出し 646: e8 95 fd ff ff call 3e0 <puts@plt> 64b: 83 c4 10 add $0x10,%esp # x86では関数の戻り値にはeaxを使用するので # eaxに0をコピー 64e: b8 00 00 00 00 mov $0x0,%eax # 退避したレジスタを復旧する 653: 8d 65 f4 lea -0xc(%ebp),%esp 656: 59 pop %ecx 657: 5b pop %ebx 658: 5e pop %esi 659: 5d pop %ebp # リターンアドレスが入っているアドレスにespを向ける 65a: 8d 61 fc lea -0x4(%ecx),%esp # return 65d: c3 ret # アライメントのための無駄な命令 65e: 66 90 xchg %ax,%ax
結構時間がかかってしまったが、やりがいのある解析だった。
argvやenvpがポインタ(4byte)として渡されているところがはっきりわかって面白かったと思う。