• 検索結果がありません。

メモリ配置

ドキュメント内 note.dvi (ページ 95-101)

6.13 コンパイルとリンク

6.13.3 メモリ配置

text セグメント.

実行可能形式のコマンド列を格納する部分.

data セグメント.

実行可能形式の大域的なオブジェクトのうち,明示的な初期化が与えられたオブジェクトが格納され る部分.

bssセグメント.

実行可能形式の大域的なオブジェクトのうち,明示的な初期化が与えられていないオブジェクトが格 納される部分.

stack セグメント.

実行中に自動変数や関数呼び出し,動的なメモリ割り当てで利用する部分.

の4つに分けられる. これらのセグメントは,次の図のように割り当てられるのが普通である.

textセグメント dataセグメント bssセグメント stackセグメント

スタック

?

ヒープ 6

命令の列 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バイトで あることがわかる.

仮にプログラム内部でこれらのセグメントを越えてメモリのアクセスを行う73と, “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) {

73これは,「ポインタ」を用いると容易に実現できる.

return a + b ; }

このプログラム中で,関数addを呼び出す直前, 呼び出した後,addの終了時のメモリ内の様子は, 以下の 通りとなる.

【注意】n,mは内部自動変数なので,メモリはすべてスタックセグメントが用いられる.

呼出し前 呼び出した後 関数終了直前

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 ; 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 関数呼出しの時には,ここで説明したものよりも多くのデータがスタックに積まれる. 関 数呼出しの時には,その時点でのレジスタ情報,関数終了時にプログラム制御が戻るべきテキストセグメン ト内のアドレス(プログラム・カウンタ)など, 多くの情報がスタックに積まれ, その後に戻り値領域, 関 数実引数領域が確保される.

また,関数実引数がスタック上に積まれる順序は処理系依存である. 実際には,オペレーティングシステ ムとライブラリ,処理系などで整合性のある渡し方が行われる74.

74多くの処理系では後ろに書かれた実引数が先にスタックに積まれることが多い. また, Pascal, Fortranなどの言語では,スタッ クに積まれる順序が指定されていて,それらで書かれたライブラリを使う場合には,処理系依存のオプションを利用することにより, スタックに実引数を積む順序を指定できることが多い.

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 ; }

この関数の呼出しは以下のように行われることがわかる.

呼出し前 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

となり,1回の呼出しで120バイトのスタックを利用していることがわかる. そこで,スタックセグメン トを小さくするために,

limit stacksize 1

とする. これにより,スタックセグメントは1Kバイトに制限される. そのうえ,func(100)を呼び出して みる. この結果はシステムに依存するが, 多くの場合, 途中で Segmentation fault というエラーを出し て実行が停止する. これは,スタックセグメントが足らなくなって, 実行が停止する例となっている.

ドキュメント内 note.dvi (ページ 95-101)