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の移植版もあるようですが未確認です。