UNIX V6とV7のcrt0.sを読んで、スタックで渡されるコマンドライン引数の形式を調べました。V7をVAXに移植したUNIX/32Vや、Interdata 7/32に移植したV6とも比較します。
※ カーネルのexecシステムコールを読む方が確実ですが、今回はアセンブリ言語を読むため敢えてcrt0.sから読み取りました。
作業時のメモをツイートしました。
argv
C言語で文字列配列の構造を見ます。
char *argv[] { "abc", "def" };
※ pre K/Rではグローバル変数への代入に =
を使用しません。
次のような構造が出力されます。
アドレス | 内容 | 備考 |
---|---|---|
0000 |
0004 |
文字列のアドレス: argv[0]
|
0002 |
0008 |
文字列のアドレス: argv[1]
|
0004 |
abc\0 |
文字列 |
0008 |
def\0 |
文字列 |
文字列のアドレスが並び、その後に文字列の実データが格納されます。main()
に渡されるargv
はカーネルが同様の構造をスタックに生成して渡します。
UNIX V6
main()
が呼ばれるまでは短いです。コメントに疑似コードで動作を示します。
mov sp,r0 / r0 = sp
mov (r0),-(sp) / sp -= 2; argc = *r0
tst (r0)+ / r0 += 2
mov r0,2(sp) / argv = r0
jsr pc,_main / main(argc, argv)
引数はスタック渡しです。起動直後とmain()
が呼ばれる直前のスタックの変化を示します。
アドレス | 起動直後 | → | アドレス | main直前 | 備考 |
---|
| |→|` (sp)`|`argc` |移動
(sp)
|argc
|→|2(sp)
|argv
|追記
2(sp)
|argv[0]
|→|4(sp)
|argv[0]
|そのまま
argc
を1つ前にずらしてargv
を挿入しています。
UNIX V7
envp
が加わったためV6より複雑になっています。コメントに疑似コードで動作を示します。
mov 2(sp),r0 / r0 = argv[0]
clr -2(r0) / *(r0-2) = 0
mov sp,r0 / r0 = sp
sub $4,sp / sp -= 4
mov 4(sp),(sp) / argc = *(sp+4)
tst (r0)+ / r0 += 2
mov r0,2(sp) / argv = r0
1: / do {
tst (r0)+ / tmp = *r0; r0 += 2
bne 1b / } while (tmp)
cmp r0,*2(sp) / if (r0 >= argv[0])
blo 1f / {
tst -(r0) / r0 -= 2
1: / }
mov r0,4(sp) / envp = r0
mov r0,_environ / environ = r0
jsr pc,_main / main(argc, argv, envp)
引数はスタック渡しです。起動直後とmain()
が呼ばれる直前のスタックの変化を示します。
アドレス | 起動直後 | → | アドレス | main直前 | 備考 |
---|
| |→|` (sp)`|`argc` |移動
| |→|`2(sp)`|`argv` |追記
(sp)
|argc
|→|4(sp)
|envp
|上書き
2(sp)
|argv[0]
|→|6(sp)
|argv[0]
|そのまま
argc
を2つ前にずらしてargv
とenvp
を挿入しています。
argv[]
とenvp[]
はNULL
で区切られています。
argv[0], ... , NULL, envp[0], ..., NULL
最初のNULL
をループ(do {} while
で示した部分)で探しています。
if
で示した部分は、もしenvp
が存在せずNULL
も1個しかなかった場合、r0 -= 2
で最初のNULL
を指すように補正しています。
NULL
がなければ誤動作するため、最後のポインタを潰してでもNULL
を作っているのがclr -2(r0)
の部分です。
UNIX/32V
V7をなるべくそのまま32bit化したような印象です。最初からコメントが入っています。
subl2 $8,sp
movl 8(sp),(sp) # argc
movab 12(sp),r0
movl r0,4(sp) # argv
L1:
tstl (r0)+ # null args term ?
bneq L1
cmpl r0,*4(sp) # end of 'env' or 'argv' ?
blss L2
tstl -(r0) # envp's are in list
L2:
movl r0,8(sp) # env
movl r0,_environ # indir is 0 if no env ; not 0 if env
calls $3,_main
引数はスタック渡しです。起動直後とmain()
が呼ばれる直前のスタックの変化を示します。
アドレス | 起動直後 | → | アドレス | main直前 | 備考 |
---|
| |→|` (sp)`|`argc` |移動
| |→|` 4(sp)`|`argv` |追記
(sp)
|argc
|→| 8(sp)
|envp
|上書き
4(sp)
|argv[0]
|→|12(sp)
|argv[0]
|そのまま
argc
を2つ前にずらしてargv
とenvp
を挿入しています。
UNIX V6 for Interdata 7/32
V6の移植版です。Interdata 7/32は32bitのビッグエンディアンのアーキテクチャです。
SIMHで動かせます。
- エミュレーション、そしてコンピューティングの歴史 2011.03.22
crt0.sを含むlibcのソースはアーカイブされたままです。
このsrc.aはGNU Binutilsと互換性がありません。
$ file src.a
src.a: old 32-bit-int big-endian archive
$ ar x src.a
ar: src.a: ファイル形式が認識できません
仕方ないので当時のarをPOSIXにやっつけで移植しました。ビッグエンディアンに依存したコードなので少し面倒でした。
これによりcrt0.sを取り出しました。
crt0 title unix c library -- runtime initialization
extrn main
extrn exit
entry _exit
r0 equ 0
sp equ 7
rf equ 15
pure
* rearrange args on stack & call main routine
sis sp,8
l r0,8(sp)
st r0,0(sp)
la r0,12(sp)
st r0,4(sp)
bal rf,main
* if main routine returns, exit
st r0,0(sp)
bal rf,exit
*
_exit equ *
l r0,0(sp)
svc 14,1
end
main()
が呼ばれるまでを抜粋して、コメントに疑似コードで動作を示します。
sis sp,8 * sp -= 8
l r0,8(sp) * r0 = *(sp+8)
st r0,0(sp) * argc = r0
la r0,12(sp) * r0 = sp+12
st r0,4(sp) * argv = r0
bal rf,main * main(argc, argv)
引数はスタック渡しです。起動直後とmain()
が呼ばれる直前のスタックの変化を示します。
アドレス | 起動直後 | → | アドレス | main直前 | 備考 |
---|
| |→|` 0(sp)`|`argc` |追記
| |→|` 4(sp)`|`argv` |追記
0(sp)
|argc
|→| 8(sp)
|argc
|そのまま
4(sp)
|argv[0]
|→|12(sp)
|argv[0]
|そのまま
argc
を2つ前にずらしてargv
を挿入しています。PDP-11やVAXと違って元のargc
を上書きしていません。
V6の移植版のためenvp
のサポートはありません。V7の移植版もあるようですが未確認です。