0から作るソフトウェア開発

日々勉強中。。。

0から作るOS開発

環境準備

環境設定

ブートローダ

カーネルローダ

GRUB

カーネル

ドライバーその他
補足説明 >

ドライバーその他 補足説明 プロセス実行時のスタック

 プロセス実行時のスタック

ここでは、GCCでコンパイルしたLinuxのアプリケーションバイナリーを
実行させるときの初期スタックの内容について説明していきます。
コンパイルして実行させるバイナリーはx86(32ビット)用を想定しています。
そして、説明を簡単にするために動的リンクではなくスタティックにリンクされた
実行バイナリーを想定しています。

プロセス実行時に必要な情報

まず初めに、実行させるプログラムについて見ていきましょう。C言語の入門書で
よく紹介されている、本当に簡単なプログラムです。次のようなmain関数です。


#include <stdio.h>

int main(int argc, char *argv[])
{
	printf("Hello, world!\n");
	return(0);
}


このmain関数ではprintf関数を呼び出して゛Hello, world!”を出力
するだけのプログラムです。簡単ですね。

このプログラムが動作し始めると、あたり前のことですが、main関数から動作を
開始します。ではこのプログラムが動作するまでに一体なにが起きているのでしょうか?

main関数の処理が開始されるまでには、GCCのglibcがmain関数を実行するための
準備を行います。更に、glibcの処理が開始される前には、カーネルが実行バイナリー
をメモリーに読み込んで、ユーザーメモリー空間の用意をします。

補足

ライブラリーをスタティックにリンクした実行バイナリーの場合はカーネルが主導的に
実行バイナリーのヘッダーを解釈してユーザー空間に読み込んでいきます。動的
にリンクされた実行バイナリーの場合は、最初にローダー(インタープリター)を
起動させます。起動するローダー(インタープリター)は実行バイナリーのヘッダー
に記録されています。起動されたローダーは実行バイナリーのヘッダーから実行時
に必要なライブラリーをメモリーに共有マップします。ローダー(インタープリター)は
OSの変わりに動的リンクが必要な実行バイナリーの実行環境を準備していきます。
メモリーにマップされた、共有ライブラリーはリンカーによって実行バイナリーにリンク
されてから、ようやくプログラム制御が実行バイナリーに移されて、プログラムが
実行されていきます。

カーネルが読み込んだ実行バイナリーはどこから処理が開始されるのでしょうか?
それは、プログラムのバイナリーフォーマットで規定されているヘッダーに記録されて
います。このバイナリーのフォーマットは色々な種類がありますが、Linuxで標準的
に使用されているELFフォーマットがよく知られています。

今回も少しだけELFについて見ていきましょう。

ELFフォーマットとプログラムの実行開始アドレス

ELFのフォーマットにはプログラムを実行するための情報がかなりたくさんあって、
非常に複雑です。詳細な仕様について見てみたい方はELFフォーマット
参照してください。

ELFの実行バイナリー(オブジェクトファイル)のフォーマットは次のように決まっています。

ELFの実行バイナリーのフォーマット

そしてプログラムの実行開始アドレス、つまり一番初めに実行が開始されるアドレスは
プログラムヘッダーに記録されています。

プログラムヘッダーを読み込むにはまずELFヘッダーを読み込む必要があります。
プログラムヘッダーが記録されている場所が格納されているからです。

ELFヘッダーは次のように定義されています。


#define EI_NIDENT        16

typedef struct
{
    unsigned char    e_ident[ EI_NIDENT ];
    Elf32_Half       e_type;
    Elf32_Half       e_machine;
    Elf32_Word       e_version;
    Elf32_Addr       e_entry;
    Elf32_Off        e_phoff;
    Elf32_Off        e_shoff;
    Elf32_Word       e_flags;
    Elf32_Half       e_ehsize;
    Elf32_Half       e_phentsize;
    Elf32_Half       e_phnum;
    Elf32_Half       e_shentsize;
    Elf32_Half       e_shnum;
    Elf32_Half       e_shstrndx;
} Elf32_Ehdr;


ELFヘッダーのe_phoffメンバーに実行ファイルの先頭からどの位置にプログラム
ヘッダーが記録されているのかが先頭からのオフセット値で格納されています。

そして、プログラムヘッダーの個数はe_phnumで、ヘッダーのサイズはe_phentsizeから
取得することができます。

補足

ELFヘッダーを持つオブジェクトファイルが実行バイナリーであることは、
e_typeがET_EXECであることで判別します。

プログラムの実行開始アドレスをプログラムヘッダーから取得する

プログラムヘッダーは次のように定義されています。


typedef struct
{
    Elf32_Word    p_type;
    Elf32_Off     p_offset;
    Elf32_Addr    p_vaddr;
    Elf32_Addr    p_paddr;
    Elf32_Word    p_filesz;
    Elf32_Word    p_memsz;
    Elf32_Word    p_flags;
    Elf32_Word    p_align;
} Elf32_Phdr;


この構造体がファイルの先頭からオフセットe_phoffの位置に配列として
格納されています(このとき当然ELFヘッダーをメモリーに読み込んだ状態です)。
Elf32_Phdr構造体の配列からp_typeがPT_LOADであるものがメモリーに
ロード可能なセグメントとなり、プログラムの実行テキストなどが格納されて
います。

ロード可能なセグメント、すなわち実行可能なプログラムのエントリーは
プログラムヘッダーのp_vaddrに格納されています。

補足

プログラムヘッダーが複数ある場合には、プログラムヘッダーはp_vaddr
を基に昇順に配列されています。

すなわち、ユーザー空間のプログラムがロードされて、一番最初に実行される
アドレスがp_vaddrとなります。

それでは、p_vaddrにはどのアドレスが格納されているのでしょうか?main関数
でしょうか?

先述した通り、main関数を実行する前にlibcで実行環境を整える準備を行い
ます。つまり、p_vaddrにはmain関数ではなくlibcの関数アドレスが記録されて
いて、その関数が最初に実行されることになります。GCCのglibcでは_start関数
がそれにあたります。

_start関数では__libc_start_main関数を呼び出します。

Linuxのi386バイナリーで呼び出される__libc_start_main関数の引数は
次のように定義されています。


int __libc_start_main(int *(main) (int, char * *, char * *),
                      int argc,
                      char **ubp_av,
                      void (*init) (void),
                      void (*fini) (void),
                      void (*rtld_fini) (void),
                      void (* stack_end));


__libc_start_main関数の引数
名前 説明
int *() (int, char * *, char * *)main 言わずと知れたmain関数のアドレスを渡します。main関数のアドレスはスタティックにリンクされたバイナリーである限り、すでにリンク済みとなりますのでコンパイル時にアドレスが決定されています。
intargc main関数に渡されるargcを渡します。
char **ubp_av main関数に渡されるargvのアドレスを渡します。
void (*) (void)init main関数が呼び出される前に呼び出されるコンストラクター関数のアドレスを指定します。
void (*) (void)fini main関数が終了される前に呼び出されるディストラクター関数のアドレスを指定します。
void (*) (void)rtld_fini この引数もmain関数が終了される前に呼び出されるディストラクター関数となりますが、カーネルが指定する関数となります。
void *stack_end プログラムが使用するスタックの終端アドレスを指定します。このアドレスからユーザープログラムはスタックに積み上げていきます。

_start関数では__libc_start_main関数に渡すこれらの引数をセットアップ
しています。ではどのように引数をセットアップして渡しているのでしょうか?
_start関数のアセンブラーを見ていきましょう。実際にスタティックにコンパイルした
バイナリーでは次のように命令が配置されていました。


08048cef <_start>:
 8048cef:       31 ed                   xor    %ebp,%ebp
 8048cf1:       5e                      pop    %esi
 8048cf2:       89 e1                   mov    %esp,%ecx
 8048cf4:       83 e4 f0                and    $0xfffffff0,%esp
 8048cf7:       50                      push   %eax
 8048cf8:       54                      push   %esp
 8048cf9:       52                      push   %edx
 8048cfa:       68 a0 96 04 08          push   $0x80496a0
 8048cff:       68 00 96 04 08          push   $0x8049600
 8048d04:       51                      push   %ecx
 8048d05:       56                      push   %esi
 8048d06:       68 6c 8e 04 08          push   $0x8048e6c
 8048d0b:       e8 70 02 00 00          call   8048f80 <__libc_start_main>
 8048d10:       f4                      hlt


アセンブラー言語に馴染みのない方は少し難しいかもしれません。
少しずつ見ていきましょう。

最初の8048cefはプログラムが配置される仮想アドレスです。これは先ほど
見てきましたプログラムヘッダーのエントリーポイントに記録されているアドレス
となります。ここから実際にプログラムの実行が開始されていきます。

最初の命令、xorでebpを0に初期化しています。
次のpop命令でスタックからesiレジスターに値をポップしています。
次のmov命令では、pop命令と同じようにスタックから値を格納していますが、
ただのmov命令となりますので、現在のスタックポインターからecxレジスターに
値を格納するだけで、スタックポインターは変更されません。

次のand命令でスタックを16バイトにアライメントしています。基本的に
i386ではスタックは16バイトにアライメントしておきたいとう方針です。
興味の無い方は特にこの処理は気にする必要はありません。

これ以降に続くpush命令は値をスタックに順次格納しています。
ここが__libc_start_mainに渡す引数となります。引数を渡した後は最後に
call命令で__libc_start_main関数を呼び出しています。

なぜ、__libc_start_main関数に渡す引数をスタックに積み上げていくので
しょうか?

それは、ABI(Application Binary Interface)、つまりこれは、i386で
実行するLinuxバイナリーは関数に渡す引数を原則的にスタックで渡すという
インターフェースの規則があるためです。この規則は、GCCではx86用のバイナリー
にコンパイルするときには、原則cdecl呼出規約を使用しています。

ではどのようにスタックで引数を渡しているのでしょうか?

次のような引数を例にあげます。


int function(int arg1, int *arg2, char arg3, double *arg4);


この関数を呼び出す前に、それぞれの引数をスタックにプッシュして呼び出します。
疑似的なアセンブラー言語で例を見ていきます。


    push arg4        // arg4に渡すアドレスをプッシュします。
    push arg3        // arg3に渡す値をプッシュします。
    push arg2        // arg2に渡すアドレスをプッシュします。
    push arg1        // arg1に渡す値をプッシュします。
    call function    // function関数を呼び出します。


このように引数の最後から順番にスタックに積んでから関数呼び出しを行います。
関数を呼び出した後、呼び出された関数でスタックを順次ポップしていきます。
これも疑似的なアセンブラー言語で見ていきます。


function:
    pop eax        // arg4をeaxレジスターにポップします。
    pop ebx        // arg3をebxレジスターにポップします。
    pop ecx        // arg2をecxレジスターにポップします。
    pop edx        // arg1をedxレジスターにポップします。
// function関数の処理が続きます。


実際にはこの例のようにはコンパイルされませんが、ここで重要なことは、スタックに
に積まれた引数をポップしているということです。実際には汎用レジスターにポップ
したり、その他のメモリーにコピーしたり様々です。しかし、概念として関数の引数は
スタックにプッシュして渡し、スタックからポップして受け取るということに違いはありません。

引数の渡し方がわかりましたので、glibcの_start関数の例と照らし合わせて
みましょう。

_start関数もアセンブラー言語で書かれていますが、C言語に相当する関数です。
引数が定義されていれば、関数の開始時にスタックから引数をポップしていきます。
それが次の処理にあたります。

[_start関数]

 8048cf1:       5e                      pop    %esi
 8048cf2:       89 e1                   mov    %esp,%ecx
 8048cf4:       83 e4 f0                and    $0xfffffff0,%esp


最初にスタックからポップしてesiレジスターに値を格納しています。これにより、_start
関数の最後の引数がesiレジスターに格納されていることになります。次のmov命令
で現在のスタックポインターが指す内容をecxレジスターに値を格納しています。
これもポップしていることとほぼ同じことになります。この場合は、つぎのand命令で
スタックポインターをメモリーアドレスが低い方に16バイトにアライメントしています。
この2命令でスタックから引数をポップしていることと等価であると考えていただいて
差し支えありません。

補足

ここで_start関数のand命令を使うことで、スタックポインターを強制的に
16バイトにアライメントしています。これは、これ以降のスタックの操作でキャッシュ
を有効に使いたいためです。CPUのL1キャッシュのサイズは通常32バイトや
64バイトなど切りのよいサイズとなっているため、頻繁に使用するスタックの
アドレスが奇数になっていたりキャッシュサイズと異なる中途半端なアドレス
を使用していると、キャッシュミスが発生しやすくなるためです。キャッシュミス
をなるべく抑えたいという考えからこのようにスタックポインターをアライメント
しています。

_start関数でスタックからポップしているのはここだけとなります。つまり、_start
関数は2つの引数を取る関数として実装されていることになります。_start関数
は受け取った引数をどのように利用するのでしょうか?次の命令を見ていきます。

[_start関数]

 8048cf7:       50                      push   %eax
 8048cf8:       54                      push   %esp
 8048cf9:       52                      push   %edx
 8048cfa:       68 a0 96 04 08          push   $0x80496a0
 8048cff:       68 00 96 04 08          push   $0x8049600
 8048d04:       51                      push   %ecx
 8048d05:       56                      push   %esi
 8048d06:       68 6c 8e 04 08          push   $0x8048e6c
 8048d0b:       e8 70 02 00 00          call   8048f80 <__libc_start_main>
 8048d10:       f4                      hlt


_start関数の続きです。最終的に__libc_start_main関数を呼び出して
います。__libc_start_main関数は先ほど見てきましたように7個の引数を
受け取ります。しかし、ここでは8回pushしています。最初のeaxレジスターを
プッシュしているのは28バイトを超えるスタックを確保したいからです。glicの
ソースコードには次のようにコメントが記載されています。

[_start関数のソースコードから抜粋]

andl $0xfffffff0, %esp
pushl %eax      /* Push garbage because we allocate
               28 more bytes.  */

/* Provide the highest stack address to the user code (for stacks
   which grow downwards).  */
pushl %esp

pushl %edx      /* Push address of the shared library
               termination function.  */


このように実装されていますので、最初のeaxレジスターのプッシュは__libc_start_main
関数が呼び出されたときには、単純に無視されることになります。他の引数について
見ていきましょう。

ここで、_start関数でプッシュする内容と__libc_star_mainが受けてる引数
の関係は次のようになります。

_start関数でプッシュする内容と__libc_start_main関数の引数
_start関数のプッシュ 引数の順番 __libc_start_main関数の引数
push $0x8048e6c1int *(main)(int, char **, char **)
push %esi2int argc
push %ecx3char **ubp_av
push $0x80496004void (*init)(void)
push $0x80496a05void (*fini)(void)
push %edx6void (*rtld_fini)(void)
push %esp7void *stack_end

ここで、第1引数のmain関数のアドレス、第4引数のinit関数のアドレス、第5引数の
fini関数のアドレスはリンク時にリンカーが決定するアドレスとなりますので、
ここでは特に注意する必要はありません。注目するのは第2引数のargc、第3引数
のubp_av、第6引数のrtld_finiとなります。第7引数のstack_endは実行時に
自動的に決まりますので、ここでは特に注意する必要はありません。

ここまで見てきましたように、第2引数のargcはesiレジスターの内容をプッシュしています。
esiレジスターの内容は_start関数実行時にスタックからポップしていました。また、
第3引数のubp_avも同様に_start関数でecxレジスターにポップしていました。
つまり、実行バイナリーを実行するとき(_start関数実行時)に、あらかじめスタックに
argcの値とubp_avのアドレスをスタックにプッシュしておく必要があります。これはローダー
またはカーネルがプッシュしておかなければならない内容となります。
また、第6引数のrtld_fini関数のアドレスも同様です。

ユーザープログラムに渡すスタックのレイアウト

では、これらの引数のためにどのような値をスタックにプッシュしていけばよいのでしょうか?
Linux(System Vに準拠)では次のようなスタックレイアウトのように配置しておく必要があります。

スタックレイアウト

スタックはこのような配置となります。_start関数実行時にはこのような配置と
なり、スタックポインターはargvのアドレスを指すようにしておきます。

もう少し具体的には次のような例となります。

スタックレイアウト例

AUXベクター

ベクターに格納するauxは実行バイナリーの補助情報を格納していきます。
これはローダーまたはOSがスタック上に用意しておきます。格納する値としては
値のタイプとその値の組み合わせで、ELFで定義されています。次のような
構造体となります。


typedef struct
{
  uint32_t a_type;              /* Entry type */
  union
    {
      uint32_t a_val;           /* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
         though, since it does not work when using 32-bit definitions
         on 64-bit platforms and vice versa.  */
    } a_un;
} Elf32_auxv_t;


この構造体は次のように単純化して考えることができます。すなわちタイプと値の定義です。
これはカーネルで定義されています。


/* Aux vector entry */
typedef struct {
	unsigned int	a_type;
	unsigned int	a_val;
} Elf32_auxv_t;


タイプは次のように定義されています。


/* symbolic values for the entries in the auxiliary table put on the initial stack */
#define AT_NULL         0       /* end of vector                                */
#define AT_IGNORE       1       /* entry should be ignored                      */
#define AT_EXECFD       2       /* file descriptor of program                   */
#define AT_PHDR         3       /* program headers for program                  */
#define AT_PHENT        4       /* size of program header entry                 */
#define AT_PHNUM        5       /* number of program headers                    */
#define AT_PAGESZ       6       /* system page size                             */
#define AT_BASE         7       /* base address of interpreter                  */
#define AT_FLAGS        8       /* flags                                        */
#define AT_ENTRY        9       /* entry point of program                       */
#define AT_NOTELF       10      /* program is not ELF                           */
#define AT_UID          11      /* real uid                                     */
#define AT_EUID         12      /* effective uid                                */
#define AT_GID          13      /* real gid                                     */
#define AT_EGID         14      /* effective gid                                */
#define AT_PLATFORM     15      /* string identifying CPU for optimizations     */
#define AT_HWCAP        16      /* arch dependent hints at CPU capabilities     */
#define AT_CLKTCK       17      /* frequency at which times() increments        */
/* 18 - 22 are reserved                                                         */

/* this entry gives some information about the FPU initialization performed by
   the kernel                                                                   */
#define AT__FPUCW       18      /* used FPU control word                        */

/* cache block sizes                                                            */
#define AT_DCACHEBSIZE  19      /* data cache block size                        */
#define AT_ICACHEBSIZE  20      /* instruction cache block size                 */
#define AT_UCACHEBSIZE  21      /* unified cache block size                     */

/* a special ignored value fo PPC, used by the kernel to control the
   interpretation of the aux vector. must be > 16                               */
#define AT_IGNOREPPC    22      /* entry should be ignored                      */
#define AT_SECURE       23      /* secure mode boolean                          */

#define AT_BASE_PLATFORM 24     /* string identifying real platform, may differ */
                                /* from AT_PLATFORM                             */

#define AT_RANDOM       25      /* address of 16 random bytes                   */
#define AT_HWCAP2       26      /* extension of AT_HWCAP                        */

#define AT_EXECFN       31      /* filename of program                          */
/* pointer to the global system page used for system calls and other things     */
#define AT_SYSINFO      32
#define AT_SYSINFO_EHDR 33

/* shapes of the caches.
   bits 0-3 contains associativity
   bits 4-7 contains log2 of line size
   mask those to get cache size                                                 */
#define AT_L1I_CACHESHAPE       34
#define AT_L1D_CACHESHAPE       35
#define AT_L2_CACHESHAPE        36
#define AT_L3_CACHESHAPE        37


auxベクターに格納する値のタイプ
タイプ 値の型または値 説明
AT_NULL0x00000000 auxベクターの最後のエントリーです。
AT_IGNORE4バイトの整数 このエントリーは値が格納されていても無視します。
AT_EXECFD4バイトの整数 実行バイナリーのオープンファイルディスクリプターです。
AT_PHDR4バイトのアドレス 実行バイナリーのプログラムヘッダーのアドレスです。
AT_PHENT4バイトのアドレス 実行バイナリーのプログラムヘッダーテーブルのアドレスです。
AT_PHNUM4バイトの整数 実行バイナリーのプログラムヘッダーの個数です。
AT_PAGESZ4バイトの整数 システムのページサイズです。
AT_BASE4バイトのアドレス インタープリターのベースアドレスです。
AT_FLAGS4バイトの整数 フラグです。現在特に使用していません。
AT_ENTRY4バイトのアドレス 実行バイナリーのプログラム開始アドレスです。通常_start関数のアドレスです。
AT_NOTELFブーリアン型(4バイト) 実行バイナリーがELFヘッダーではないことを示します。
AT_UID4バイトの整数 プロセスのユーザーIDです。このエントリーが無いとglibcはgetuidシステムコールを使ってユーザーIDを取得します。
AT_EUID4バイトの整数 プロセスの実効ユーザーIDです。このエントリーが無いとglibcはgeteuidシステムコールを使って実効ユーザーIDを取得します。
AT_GID4バイトの整数 プロセスのグループIDです。このエントリーが無いとglibcはgetgidシステムコールを使ってグループIDを取得します。
AT_EGID4バイトの整数 プロセスの実効グループIDです。このエントリーが無いとglibcはgetegidシステムコールを使って実効グループIDを取得します。
AT_PLATFORM4バイトのアドレス プラットフォームを示す文字列が格納されているアドレスです。"i386"などをスタックなどに格納しておき、そのアドレスを格納しておきます。
AT_HWCAP4バイトの整数(ビット定義) ハードウェアで利用可能な能力をビットで示します。
AT_CLKTCK4バイトの整数 time()で1回にカウントする周波数が格納されます。
AT__FPUCW4バイトの整数 FPU制御ワードを格納します。現在i386バイナリーでは使用していません。
AT_DCACHEBSIZE4バイトの整数 データキャッシュのブロックサイズを格納します。現在i386バイナリーでは使用していません。
AT_ICACHEBSIZE4バイトの整数 命令キャッシュのブロックサイズを格納します。現在i386バイナリーでは使用していません。
AT_UCACHEBSIZE4バイトの整数 共通(L2)キャッシュのブロックサイズを格納します。現在i386バイナリーでは使用していません。
AT_IGNOREPPC4バイトの整数 Power PC用のエントリーです。現在i386バイナリーでは使用していません。
AT_SECUREブーリアン型 セキュアモードの有効/無効を示します。
AT_BASE_PLATFORM4バイトのアドレス プラットフォームを示す文字列のアドレスが格納されます。
AT_RANDOM4バイトのアドレス 実行時にアドレス空間をランダムに配置するときに使用します。
AT_HWCAP24バイトの整数(ビット定義) ハードウェアで利用可能な能力をビットで示します。AT_HWCAPの拡張ビットです。
AT_EXECFN4バイトのアドレス 実行バイナリーのファイル名が格納されているアドレスが格納されます。このエントリーが定義されていないとglicはファイル名を/proc/self/exeから取得します。
AT_SYSINFO4バイトのアドレス vDSOの_kernel_vsyscallのアドレスを格納します。
AT_SYSINFO_EHDR4バイトのアドレス vDSOのELFヘッダーのアドレスを格納します。vDSOの各種関数を呼び出すために使用します。

補足

auxベクターの値はgetauxval(3)で取得することができます。

vDSOについて少し説明

vDSOはvirtual Dynamic Shared Objectと言います。実行時にリンクされる
共有ライブラリーとなります。当初int命令より高速に動作するsysenter命令が導入された
ときに、libcからsysenter命令を呼び出すために、_kernel_vsyscall関数が
実装されました。sysenter命令を使ったシステムコール呼び出しは基本的にOS依存の
処理となりますので、OSごとに個別の実装となります。また、sysenter命令に対応して
いない旧OSへの実行バイナリー互換性も考慮して、sysenter命令に対応している
場合はAT_SYSINFOにsysenter命令によるシステムコール呼び出し処理、つまり、
_kernel_vsyscall関数を実行時にリンクしてアドレスを登録します。
これにより、実行時にlibcはauxベクターにAT_SYSINFOがあるか調べ、無い場合、
OSはsysenter命令に対応していませんので、int命令によるシステムコール呼び出し
を行います。ある場合は、AT_SYSINFOに登録されている_kernel_vsyscall関数を
呼び出してsysenter命令によるシステムコール呼び出しを行います。

更に、システムコール呼び出しにはオーバーヘッドが大きいため、頻繁に呼び出される
システムコールについては同じように実行時にユーザー空間にリンクしています。これにより
特定のシステムコールの呼び出しは通常の関数の呼び出しと同じ効率で処理できる
ようになりました。例えば、gettimeofdayシステムコールがそれにあたります。
gettimeofdayシステムコールがユーザープログラムから呼び出されると、共有リンク
されているgettimeofdayと同じ処理内容を行う関数が呼び出されます。これらの関数
がvDSOライブラリーとしてリンクされています。

i386のvDSOでリンクされるのは次のようなインターフェースとなります。


__kernel_sigreturn
__kernel_rt_sigreturn
__kernel_vsyscall
__vdso_clock_gettime
__vdso_gettimeofday
__vdso_time


_start関数説明続き

それではスタックのレイアウトが分かりましたので_start関数の説明の続きです。
(続きといいますか、復習です)。

_start関数の最初ではスタックから値をポップしています。

[_start関数]

 8048cf1:       5e                      pop    %esi
 8048cf2:       89 e1                   mov    %esp,%ecx
 8048cf4:       83 e4 f0                and    $0xfffffff0,%esp


このときesiレジスターにargcが、ecxレジスターにargvのアドレスが格納される
ことになります。

そして、次に__libc_start_main関数に渡す引数のプッシュを行います。

[_start関数]

 8048cf7:       50                      push   %eax
 8048cf8:       54                      push   %esp
 8048cf9:       52                      push   %edx
 8048cfa:       68 a0 96 04 08          push   $0x80496a0
 8048cff:       68 00 96 04 08          push   $0x8049600
 8048d04:       51                      push   %ecx
 8048d05:       56                      push   %esi
 8048d06:       68 6c 8e 04 08          push   $0x8048e6c
 8048d0b:       e8 70 02 00 00          call   8048f80 <__libc_start_main>
 8048d10:       f4                      hlt


eaxレジスターのプッシュは特に意味はありません。
espレジスターのプッシュでstack_end引数に現在のスタックポインターを渡します。
edxレジスターのプッシュでrtld_fini引数にアドレスを渡します。
次のアドレスのプッシュはinit引数とfini引数にそれぞれアドレスを渡します。
ecxレジスターのプッシュはenvpのアドレスを渡しています。この段階で渡しているのは
argvのアドレスですが、後で__libc_start_main関数でargvのエンドマーカーを検出
してenvpのアドレスを計算します。
そしてesiレジスターでargcの値をプッシュして渡しています。
最後のプッシュはmain関数でした。

スタックレイアウトの図と見比べながら引数に何を渡しているのかを見てください。

ここで、edxレジスターは_start関数が始まって以降値は更新されずにプッシュされて
います。つまり、_start関数が呼び出されたときにはすでにedxレジスターにrtld_fini
のアドレスが格納されていることを期待しているということになります。ローダーまたはOSが
edxレジスターにあらかじめアドレスをセットしておく必要があることに注意してください。
rtld_finiはat_exit

余談ですが、Linuxではプログラム実行する前に各レジスターを0初期化します。
そのあとディストラクターが必要であればedxレジスターにアドレスを格納します。
edxレジスターにNULLを格納しておくとexit時にディストラクターは呼び出されません。

(終わり)
inserted by FC2 system