FAQ - 2024年度 システムプログラミング
FAQ
配列とスタックに関する質問
質問1 関数終了時に壊れるデータをスタックに格納してはいけないのでしょうか.例えば,Practice5 で素数を格納する配列として使うことができますか.
スタックは,その関数の終了時に開放されるので, 関数が終わるまでに使用を終えなければなりません.例えば,
int primes[100]; int main() { : }
となっているところを
int main() { int primes[100]; : }
とすることで,配列をスタック上に確保することができますが,
main
関数が終わるまでの寿命です.この例では正しく動きますね.なぜなら,
main
が終われば,
primes
が消えてもよいので.
別の関数で配列をスタック上に確保して, その配列を別の関数に戻り値として渡すといったようなことはできません.
また,気をつけなければいけないのは, スタック上に確保された変数や配列は,その関数が呼び出される際に毎回,値が消えてしまうということです. この話は,教科書2.18節「誤信と落とし穴」に解説されています.
また,スタックは,あまりに大きな領域を確保するのに向いていません. 関数が再帰的に呼び出されたりすると,スタックを大量に消費することも覚えておく必要があります.
命令に関する質問
質問1 スタック中の値は, $sp
の相対で表現できるので, $fp
をわざわざ使うメリットがよく分かりません
$fp
なしで, $sp
からの相対番地だけでプログラムを記述することは,人間にとってそう難しくありません.
ですから, $fp
の意味に疑問を感じて当然だと思います.
しかし,確保したローカル変数にアクセスするコードをコンパイラで「機械的」に生成することを考えると,
常に $fp
と $sp
の間に挟まれる領域が使用可能だとしておく方が,コンパイラの作成自体を簡単にできます.
これは3年生のコンパイラ作成実験で実感するでしょう.
更に, $sp
と $fp
の2つでスタックの両端を保存していると,
プログラムが異常終了したときに,スタックの量とその中身を調べることが簡単になります.
デバッガなどを用いた障害解析の際に役立つことがあります.
ちなみに,gcc (コンパイラ)では, $fp
を使用しないようにする最適化オプション
-fomit-frame-pointer
があり,用途によって使い分けることができます.
質問2 スタックの解放は, $sp
を移動させることで実現していますが,それでは,メモリ中に値が残ったまま電力の無駄になりませんか.
PCは,メモリ中にどんな値が入っていようと,(たとえ0でクリアされていようと)電源が入っている間は, その値を保持しつづけます.そのため,消費電力は変わりません.
スタック開放時にメモリを0にクリアすると,そのぶん動作が増えるので,実行速度が低下します. 従って,開放時には前の値をそのまま放置しているのです.
C言語の関数内で通常の方法で宣言される変数(auto 変数といいます) は,スタック上に確保されるので, そのまま使うと,初期値が不定なのもこのためです.
以上のように,スタックは,解放しても直ちに値が消える訳ではなく,
他の手続きが上書きするまでは,前の値が残っているものなのです.
解放したスタックに残っている値を使用してしまう誤りは,
dangling pointer
として知られています.
質問3 スタックを利用するときに,なぜ $sp + 16
から使い始めるのでしょうか.前4つを無駄に空けているのは,何のためでしょうか.
Practice6で解説します.
質問4 割り算命令の結果は,なぜ2つの変なレジスタに入るのですか.
割り算は商と剰余の2つの値を返します.
MIPS の設計では,この2つの値は,
hi
と lo
と呼ばれる特別なレジスタに保存されます.
さらに, hi
, lo
レジスタから値を取り出すときには,
通常の方法ではなく専用の命令を利用します.したがって,
$a0 / $a1
の剰余を $v0
に代入するためには,下記のようになります.
div $a0, $a1 # $a0 / $a0 の 商を lo 剰余を hi レジスタに mfhi $v0 # move from hi -- hi の値を $v0 にコピー
質問5 解答例で,syscall のときに使う $v0
を slt
や beq
の所で使用してもいいのですか.
教科書には,「用途」の所に「式の評価と関数の結果」とあります. $v0
の中にある値は常に変化してもいいのでしょうか.
手続き呼び出し規約というのは,手続き(関数)の入口と出口の間でのレジスタやスタックの変化について規定するものです.
逆にいうと,手続きの入口と出口で規約が守られていれば,手続き中ではレジスタをどう使っても自由であるという意味です.
ですから, $v0
は,関数の中で自由に使って,「最後に」その関数としての返り値が $v0
に入っていれば,
途中で $v0
をどう使っていても構いません.
例では, syscall
で呼び出す print_int
も fact
も個々の
関数であると考えると,両者ともに $v0
に戻り値を入れて返すように動作していると思います.
質問6 li
命令と la
命令は,どう違うのですか
la $a0, label
と li $a0, Imm
は,
第2オペランドが「ラベル」と「即値」といった違いがありますが,
実行時には, label
も Imm
も 32ビットの定数です.
したがって, $a0
レジスタに 32ビットの定数を代入するという意味では,同じではないかという疑問が湧きますね.
(教科書B.10節「MIPS R2000 のアセンブリ言語」の「アドレッシングモード」参照)
実は,両者は,最終的には同じ機械語に置き換わるので,同じといえば同じです.
それなのに,SPIM のアセンブラは,ラベルに対して li
命令を使うと,文法エラーを出して,機械語に変換してくれません.
これは,書いた人の意図を明確に表現するために両者を使い分けて欲しいということだと思います.
一方, $a0
などのレジスタは,即値ではないので,
li $a1, $a0
とは書けませんが, la
は,la $a1, $a0
や la $a1, 10($a0)
などとも書けるところが li
と違っています.
SPIM, gcc に関する質問
質問1 SPIMでプログラムが動作しないのですが.
以下を確認してみてください.
- gcc や qtspim を正しく起動していますか.
qtspim には
-mapped_io
オプションが必要です. スペルを間違えていてもエラーにならないので,特に注意が必要です.- 自宅のWindows PCなどで GUI の QtSpim を動かしている場合は,
-mapped_io
に相当するオプション設定が GUI の Settings 項目に Enable Mapped IO としてあります. - なお Enable Mapped IO は,Practice3 以降では,不要なので off にしておいたほうがいいようです.on にしておくと QtSPIM の動作に不具合があるようです.
- 自宅のWindows PCなどで GUI の QtSpim を動かしている場合は,
- プログラム中にスペルミスをしていませんか.
よく
align
をaligh
と書いたりしている人がいます. プログラムの内容以前の部分を確認してみてください. これは qtspim の下部にエラーメッセージとして表示されているはずです. - ファイルをloadする順番は正しいですか. 一部の課題では,複数のアセンブラファイルをロードしてから実行する必要があります. その順番を指定された通りにしていますか.
- 実行後にメモリ中を破壊しているにもかかわらず,再度 (run) しようとしていませんか. 怪しいなと思ったら, (clear) → (memory & registers) を実行して,メモリを綺麗にしてから,再 (load) してみましょう.
- あなたが load や編集しようとしているファイルは,本当に目的のファイルですか. spim が参照しているディレクトリと,emacs が参照しているディレクトリ, ウェブブラウザのダウンロード先などが違うことによる勘違いが考えられます.
質問2 SPIM では, main:
のラベルから始めないといけないのはなぜですか.
SPIM では,電源が入ると (起動すると),最初に exceptions.s
というファイルを読み込むようになっています.
exceptions.s
は,スタックの初期化,
割り込みや例外のテーブルの作成などの後,
main:
を呼び出すようになっています.
以下は, exceptions.s
中で main:
を呼び出す部分の抜粋です.
.text .globl __start __start: lw $a0 0($sp) # argc addiu $a1 $sp 4 # argv addiu $a2 $a1 4 # envp sll $v0 $a0 2 addu $a2 $a2 $v0 jal main # main を呼び出し nop li $v0 10 syscall # syscall 10 (exit) .globl __eoth __eoth:
質問3 spim-gcc を自宅のPCで使いたいのですが.
spim-gcc は演習室でしか利用できませんが,自身のPCにインストールした gcc である程度動作を確認することができます.
演習の後半,とくに mypirntf の実装においては,自分の gcc で myprintf の動作をある程度確認をしてから, spim-gcc でコンパイルすると効率がよいでしょう.
自宅PCで myprintf を作る際に困るのは,syscall.s として作成した print_string
や print_int
がないことでしょう.
これは,以下のようなヘッダファイルで代替できます.以下のファイルを spim.h
として保存して,
#ifndef __SPIM_H__ #define __SPIM_H__ #include <stdio.h> #include <stdlib.h> #define print_int(i) printf("%d", (i)) #define print_string(s) printf("%s", (s)) int read_int(void) { char buf[1024]; int n; if (fgets(buf, sizeof(buf), stdin) == NULL) return 0; sscanf(buf, "%d", &n); return n; } void read_string(char *buf, int n) { (void) fgets(buf, n, stdin); } #endif /*__SPIM_H__*/
これを 以下のように include することで,自宅の gcc でも print_string
や print_int
を使った
Cプログラムを作成できるでしょう.以下のプログラムを test.c として保存して,
#include "spim.h" int main() { char buf[100]; int n; print_string("Input string: "); read_string(buf, 100); print_string(buf); print_string("Input number: "); n = read_int(); print_int(n); print_string("\n"); }
コンパイルして実行できます.
gcc -m32 test.c ./a.out
このような工夫をしてみてください.
ただし,みなさんのPCのCPUは,64bit CPU かもしれません.SPIM は,32bit CPU なので,
コンパイル時に 32bit コードを出すようにしたほうがいいでしょう.コンパイルには -m32
オプションが必要です.
また,当然ですが, printf
のような便利な関数を使ってしまうと,自宅の PC ではコンパイルできても,
spim-gcc ではコンパイルできません.spim には事前に準備された関数が何もないのだということを忘れないでください.
C言語プログラミングに関する質問
その他の質問
質問1 レポート中にプログラムを掲載するときに,行番号を付ける方法のが手間なので,何とかしたい.
$ cat -n practice1-1.s
などとすると,行番号付きで出力されるので,リダイレクトでファイルに保存するなりすれば,楽になります.
質問2 レポート中にプログラムを掲載するときに,タブが入っているとインデントがずれるのを何とかしたい.
TeX の verbatim 環境を使う場合は,タブをスペースに変換しておく必要があります.
emacs では,変換する領域(リージョン)を指定して,
M-x untabify
(Esc
を押して x
の後 untabify
で ENTER
)
と入力することで,タブをスペースに置き換えることができます.