第 6 章 C 言語入門
6.13 コンパイルとリンク
6.13.3 メモリ配置
当然, “a is not lower character”という出力を得る. これだけであれば,正しく動作するのだが,もし, 他の標準関数でislower関数を利用している関数67 を利用したらどうなるのだろうか?この場合には,標 準ライブラリのislowerではなく,このプログラムファイル中にあるislowerが利用される. ということ は, 悲惨な結末を向かえることになるのは明らかである.
このように,標準関数内で定義されているシンボル名を関数名に利用してはいけない.
• textセグメント.
実行可能形式のコマンド列を格納する部分.
• dataセグメント.
実行可能形式の大域的なオブジェクトのうち,明示的な初期化が与えられたオブジェクトが格納される 部分.
• bssセグメント.
実行可能形式の大域的なオブジェクトのうち,明示的な初期化が与えられていないオブジェクトが格納 される部分.
• stackセグメント.
実行中に自動変数や関数呼び出し,動的なメモリ割り当てで利用する部分.
の4つに分けられる. これらのセグメントは,次の図のように割り当てられるのが普通である.
textセグメント dataセグメント bssセグメント stackセグメント
スタック
❄
ヒープ
✻
命令の列 j l t i k s n(main呼出し時に) m(main呼出し時に)
text セグメント, data セ グメントは, 主記憶への配 置時に, 実行ファイル中に ある値で埋め尽くされる.
bssセグメントは実行開始 時に0でクリアされる.
stack セグメントは, 関 数呼び出しに伴い, 上位メ モリから順に利用される.
(スタックの利用)また,動 的メモリ割り当てのうち alloca関数では,スタック の利用が可能である.
実行中の動的メモリ割り当
て (malloc 関数の呼出し
など) では, stack セグメ ントが下位メモリから順に 利用される. (ヒープ領域 の利用)
sizeコマンドで,a.out の各セグメントの大きさを調べると,
% size a.out
2288 + 360 + 68 = 2716
となり,text セグメントが2288バイト,dataセグメントが 360バイト,bssセグメントが68バイトで あることがわかる.
Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp
仮にプログラム内部でこれらのセグメントを越えてメモリのアクセスを行う70と, “segmentation fault”
という実行時エラーを発生して,プログラムの実行が停止する.
stackセグメントは,次に述べる関数呼び出し手順の中で利用され,stackセグメントにどれだけの大き
さが割り当てられるかは,実行形式を呼び出したシェルの環境に依存する. stackセグメントの大きさは
limitコマンドで表示される値
cputime unlimited filesize unlimited datasize 2097148 kbytes stacksize 8192 kbytes coredumpsize 0 kbytes vmemoryuse unlimited descriptors 64
で知ることが出来る.
6.13.4 関数呼び出しの手順
Section 6.11.1.7 では,関数呼出しを行った場合のプログラムの動作の様子を考察し, 関数への実引数は
「値渡し」が行われることを述べた. ここでは,それがメモリ内で何をしていることになるのかを考察して みよう.
6.13.4.1 関数実引数の渡し方
ここでは,次のようなプログラムを例としよう.
extern int add(int, int) ; int main(int argc, char **argv) {
int n, m ; n = 1 ; m = 2 ; n = add(n,m) ; return 0 ; }
int add(int a, int b) {
return a + b ; }
このプログラム中で,関数addを呼び出す直前,呼び出した後,addの終了時のメモリ内の様子は,以下の 通りとなる.
【注意】n,mは内部自動変数なので,メモリはすべてスタックセグメントが用いられる.
70これは,「ポインタ」を用いると容易に実現できる.
Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp
呼出し前 呼び出した後 関数終了直前 main のn,値1
main のm,値2
mainの n,値 1 mainの m,値 2 add の戻り値を格納する部分
add の a,値 1 add の a,値 2
✲
値のコピー ✛
値のコピー
main のn,値 1 main のm,値 2
値3 add のa,値 1 add のa,値 2 main に戻ってきたとき 関数呼出し終了
main のn,値1 main のm,値2
戻り値3
mainの n,値 3 mainの m,値 2
この図のように,関数呼出しを行うと,関数の実引数はスタック内に新しい記憶領域が確保され,そこへ実 引数の値がコピーされる. したがって,
extern int add(int) ;
int main(int argc, char **argv) {
int n ;
n = 1 ; add(n) ; return 0 ; }
int add(int a) ; {
return ++a ; }
というプログラムでは,
呼出し前 呼び出した後 関数終了直前
main のn,値 1 mainの n,値 1 add の戻り値を格納する部分
add の a,値 1
main のn,値1 値2 add のa,値2 mainに戻ってきたとき 関数呼出し終了
main のn,値 1 戻り値 2
mainの n,値 1
となり,nの値が変化しない理由は明らかとなる. しかし,ファイルスコープのオブジェクトは静的であり, データセグメントまたはbssセグメントに格納されるため,それらを関数内で変更すると,その変更は永続 的となる.
extern int add(int) ; int k = 0 ;
int main(int argc, char **argv) {
int n = 1 ; n = add(n) ; return 0 ; }
int add(int a) ; {
int b = 1 ;
Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp
b += 1 ; k += 1 ; return ++a ; }
というプログラムの場合には,
呼出し前 呼び出した後 return文実行直前 mainのn,値 1
k,値 0
mainのn, 値1 add の戻り値を格納する部分
add のa, 値1 add のb, 値1
k,値0
main のn,値1 add の戻り値を格納する部分
add のa,値1 add のb,値2
k,値1 関数終了直前 mainに戻ってきたとき 関数呼出し終了 mainのn,値 1
戻り値2 add のa,値 2 add のb,値 2
k,値 1
mainのn, 値1 戻り値2
k,値1
main のn,値2
k,値1 となり,kの値は変更されている. (kはデータセグメントに格納されている.)
Remark 6.13.1 関数呼出しの時には,ここで説明したものよりも多くのデータがスタックに積まれる. 関 数呼出しの時には,その時点でのレジスタ情報,関数終了時にプログラム制御が戻るべきテキストセグメン ト内のアドレス(プログラム・カウンタ)など, 多くの情報がスタックに積まれ,その後に戻り値領域,関 数実引数領域が確保される.
また,関数実引数がスタック上に積まれる順序は処理系依存である. 実際には, オペレーティングシステ ムとライブラリ,処理系などで整合性のある渡し方が行われる71 .
6.13.4.2 再帰的関数呼出しの様子
次に再帰的関数呼出しを行う時の様子を見てみよう. Example 6.11.7で用いた,帰納的に定義された数 列an+1=an+ 2,a0= 0の an を求める関数を利用しよう.
extern int func(unsigned int) ; int main(int argc, char **argv) {
recursive_function(2) ; return 0 ;
}
int func(unsigned int n) {
if (n == 0) return 0 ; return func(n-1) + 2 ; }
この関数の呼出しは以下のように行われることがわかる.
71多くの処理系では後ろに書かれた実引数が先にスタックに積まれることが多い.また, Pascal, Fortranなどの言語では,スタッ クに積まれる順序が指定されていて,それらで書かれたライブラリを使う場合には,処理系依存のオプションを利用することにより, スタックに実引数を積む順序を指定できることが多い.
Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp
呼出し前 func(2)呼び出し後 func(1)呼び出し後 (func(2)の戻り値)
(func(2)の実引数) 2
(func(2)の戻り値) (func(2)の実引数)2
(func(1)の戻り値) (func(1)の実引数)1
func(0)呼出し後 func(0)終了 func(1)終了
(func(2)の戻り値) (func(2)の実引数)2
(func(1)の戻り値) (func(1)の実引数)1
(func(0)の戻り値) (func(0)の実引数)0
(func(2)の戻り値) (func(2)の実引数) 2
(func(1)の戻り値) (func(1)の実引数) 1 (func(0)の戻り値) 0
(func(2)の戻り値) (func(2)の実引数)2 (func(1)の戻り値)2
func(2)終了 関数呼出し 終了
(func(2)の戻り値)4
各func()終了時には, return func(n-1) + 2
が行われ,直前の戻り値に2を加えたものがその関数の戻り値となる. このことから,再帰的な関数呼出し がスタックを順に利用していることがわかる.
6.13.4.2.1 再帰的関数呼出しでスタックをあふれさせる さて,再帰的関数呼出しを実行して,スタック
領域が使い尽くされていくことを実感するために,以下のような「荒っぽい」ことをしてみよう.
上で利用した関数funcを大量に呼び出して,スタックセグメントが使い尽くされると何が起こるだろう か?まず,以下のプログラムを実行してみよう.
#include <stdio.h>
extern int func(unsigned int) ; int main(int argc, char **argv) {
func(10) ; return 0 ; }
int func(unsigned int n) {
char c ;
printf("n = %3d, c = %p\n",n,&c) ; if (n == 0) return 0 ;
return func(n-1) + 2 ; }
ここで, c = %pの出力は, &c, すなわち,cのアドレスを出力する. したがって,この値はその時点でのお およそのスタックの先頭のアドレスを示していることになる. 実行結果は,
n = 10, c = effff9a7 n = 9, c = effff92f n = 8, c = effff8b7 n = 7, c = effff83f n = 6, c = effff7c7 n = 5, c = effff74f n = 4, c = effff6d7 n = 3, c = effff65f n = 2, c = effff5e7 n = 1, c = effff56f n = 0, c = effff4f7
Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp
となり,1回の呼出しで120バイトのスタックを利用していることがわかる. そこで,スタックセグメン トを小さくするために,
limit stacksize 1
とする. これにより,スタックセグメントは1Kバイトに制限される. そのうえ,func(100)を呼び出して みる. この結果はシステムに依存するが, 多くの場合, 途中でSegmentation faultというエラーを出し て実行が停止する. これは,スタックセグメントが足らなくなって, 実行が停止する例となっている.