6. コマンドライン引数と環境変数の処理
アセンブラで作成されたプログラムは,環境変数やコマンドライン引数 にどのようにアクセスしたらいいのでしょうか?
プログラムが起動された瞬間のスタックの内容を簡単に示します. この図から環境変数とコマンドライン引数の格納のされ方がわかります. 実際にスタックの内容を表示するプログラムも作ります.
+------------------------+ アドレス下位 | | ↑スタックの伸びる方向 +------------------------+ sp | argc | スタックトップ +------------------------+ argv | char *argv[0] | argc = 2 とすると sp-=argc+1 +------------------------+ | char *argv[1] | +------------------------+ | 0 | +------------------------+ envp | char *env[0] | envc = 2 とすると sp-=envc+1 +------------------------+ | char *env[1] | +------------------------+ | 0 | +------------------------+ : : : : +------------------------+ | i686 | +------------------------+ | | <------- bprm->p +------------------------+ | current->mm->arg_start | コマンドライン引数の文字列 +------------------------+ : : : : +------------------------+ | current->mm->env_start | これ以下は環境変数の文字列 +------------------------+ : : : : +------------------------+ | current->mm->env_end | [203] +------------------------+ アドレス上位(スタックボトム側)
カーネルソースの fs/binfmt_elf.c の create_elf_tables() 関数中で __put_usr がユーザ空間に書きこんでスタックが用意されます.
シェルがプログラムを起動する場合,まず,fork のあとプログラムを ロードして実行するシステムコール execve が呼び出されます. そして最終的に fs/binfmt_elf.c の do_load_elf_binary でスタックを 用意(create_elf_tables)して,start_thread で実行が開始されます.
プログラムに渡す引数を,プログラム側でどのようにに受け取ればいい のでしょうか? C のプログラムでは,main 関数の引数 argc と argv でコマンドライン引数を受け取ることができます.アセンブラで書く場 合もコマンドライン引数へのポインタとしてスタックから取得します.
アセンブラで作成したプログラムから見たコマンドライン引数は, スタックトップに引数の数,次にプログラム自体の名前,続いて 引数文字列へのポインタが順に格納されています.最後はヌルポインタ で終了しています. 同様に環境変数文字列へのポインタ群とヌルポインタ が続きます.
コマンドライン引数無しで呼び出されたプログラムも最低,引数の数, 最初の引数としてのプログラム自体の名前,そしてヌルポインタが スタックトップに存在します.
さて実際にプログラムに渡すコマンドライン引数を確認するプログラム を書いてみます.
;---------------------------------------------------------------------- ; command line arguments ; 2000/ 4/24 cmdline.asm ; ; Copyright (C) 2000 Jun Mizutani <mizutani.jun@nifty.ne.jp> ; ; usage : cmdline abcd lkl k p ; ; No. of Argument <Stack top> : 5 ; Program Name : ./cmdline ; Argument #1 Address : BFFFFCCF Argument String : abcd ; Argument #2 Address : BFFFFCD4 Argument String : lkl ; Argument #3 Address : BFFFFCD8 Argument String : k ; Argument #4 Address : BFFFFCDA Argument String : p ; %include "stdio.inc" section .text global _start m_argc db " No. of Argument <Stack top> : ", 0 m_cmd db " Program Name : ", 0 m_args db " Argument #", 0 m_args2 db " Address : ", 0 m_arg_c db " Argument String : ", 0 _start: call NewLine mov eax, m_argc call OutAsciiZ pop eax ; 引数の数 argc call PrintLeft call NewLine dec eax ; 実際の引数の数 pop ebx ; コマンド名取得 mov eax, m_cmd call OutAsciiZ mov eax, ebx call OutAsciiZ ; コマンド名表示 call NewLine xor ebx, ebx .next_arg: inc ebx pop edi ; pop filename pointer or edi, edi ; check null pointer jz .done ; end of args processing mov eax, m_args call OutAsciiZ mov eax, ebx call PrintLeft mov eax, m_args2 call OutAsciiZ mov eax, edi call PrintHex8 mov eax, m_arg_c call OutAsciiZ mov eax, edi call OutAsciiZ ; 引数表示 call NewLine ; 改行 jmp short .next_arg ; try next arg .noarg .done call NewLine ; 改行 call Exit ;----------------------------------------------------------------------
スタックから次々に pop して引数の文字列へのポインタを取得して 表示しています. push と pop が対応していない,少し気味が悪い プログラムになっていることに気づきましたか?
Cの関数やアセンブリコードのサブルーチンでは,スタック経由で データが渡された場合,処理が終了して戻る場合(または戻った直後) に,スタックの状態を正確に元に戻しておく必要があります.
Linuxのプロセスとして起動されたプログラムは,終了時に exit システムコールで終了し,そのプロセスは割り当てられたメモリとと もに消滅するため,開始時と終了時のスタックの整合が取れていなく ても問題はありません.したがってプログラム側では,引数を必要と しない場合にはスタックを処理する必要はありません.
次のプログラムはスタックの内容のダンプリストと環境変数のリストを 表示します.ローカルラベルの .dispenv から .endenv の部分で環境 変数を順次表示しています.
;---------------------------------------------------------------------- ; Stack Dump 2000/ 4/24 ; Copyright (C) 2000 Jun Mizutani <mizutani.jun@nifty.ne.jp> ; name : stackdump.asm ; usage : stackdump dummyargs .. ;---------------------------------------------------------------------- section .bss dummy resd 1 section .data msg db "Program Counter : ", 0 msg1 db "Data Section : ", 0 msg2 db "Stack Pointer : ", 0 msg3 db "BSS Section : ", 0 section .text global _start _start: call .skip .skip pop eax ; Set IP into eax mov ebx, eax mov eax, msg call OutAsciiZ mov eax, ebx call PrintHex8 ; print IP call NewLine mov eax, msg1 call OutAsciiZ mov eax, msg call PrintHex8 ; print data section address call NewLine mov eax, msg3 call OutAsciiZ mov eax, dummy call PrintHex8 ; print bss section address call NewLine mov eax, msg2 call OutAsciiZ mov eax, esp call PrintHex8 ; print Stack Pointer call NewLine call NewLine mov ebp, esp mov ebx, [ebp] ; argc add ebx, 2 shl ebx, 2 add ebp, ebx ; skip argv .dispenv: mov eax, [ebp] or eax, eax jz .endenv call OutAsciiZ ; env call NewLine add ebp, 4 jmp short .dispenv .endenv call NewLine .displine: mov eax, ebp call PrintHex8 mov al, ':' call OutChar mov ecx, 16 push ebp .loop1: cmp ebp, 0xC0000000 jae .next mov al, [ebp] call PrintHex2 mov al, ' ' call OutChar inc ebp loop .loop1 .next: mov al, ' ' call OutChar mov ecx, 4 pop ebp .loop2: cmp ebp, 0xC0000000 jae .exit mov eax, [ebp] call OutChar4 ; print stack contents in ASCII add ebp, 4 loop .loop2 call NewLine jmp .displine .exit call NewLine call Exit %include "stdio.inc" ;----------------------------------------------------------------------
実行するとコマンドライン引数も環境変数も文字列の実体はスタック上に 積まれていることが確認できます.
環境変数もスタックを pop しながら取得することができますが,この例では pop することなくスタックをたどって環境変数(文字列)を取得しています.
先頭にある以下のコードはラベル .skip のアドレスを EAX に設定するトリック です.プログラムがどの辺のアドレスで動作しているかが分かります.
call .skip .skip pop eax ; Set IP into eax
ここでは %include "stdio.inc" を最後においていますが,問題無く動作する ことが確認できます. NASM でも前方参照が可能です.