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 でも前方参照が可能です.