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