①メモリとスタック
メモリ
さて、すべてのプログラムはメモリ上にデータをのせて実行するわけなのだが、
プログラム全体には5つの領域(セグメント)に分けられて実行される。
スタックもその1つである。
-1テキストセグメント(コードセグメント) ここにマシン語で記述されたプログラムが格納される。またマシン語コードのみで変数などが格納されることはない。変更を防ぐため書き込みが禁止されている。 -2データセグメント 初期化済みのグローバル変数や静的変数、文字列、定数を格納。 -3 bssセグメント 初期化されていない変数を格納。 -4ヒープセグメント プログラム中に動的に割り当てられる変数を格納。動的なので必要に応じて収縮拡張出来る。 -5スタックセグメント ここにスタックが格納されている。 (参考文献) HACKINGーTHE ART OF EXPLOITATION-
ポインタはメモリアドレスなので実際にメモリアドレスを並べてみる。
#include<stdio.h> #include<stdlib.h> int g_var; int g_var2=100; static int s_var; const int c_var=100; void fanc(int arg) { int l_var; static int s_var2; printf("%10p=&s_var2\n",&s_var2); } int main(int argc,char **argv,char **envs) { int l_var; static int s_var3=100; char *pc="ABCDEFG"; char *pc2="1234567"; char *mp=malloc(10); char *mp2=malloc(10); printf("ローカル変数\n"); printf("%10p=&envs\n",&envs); printf("%10p=&argv\n",&argv); printf("%10p=&argc\n",&argc); printf("%10p=&l_var\n",&l_var); printf("mallocしたもの\n"); printf("%10p=mp2\n",mp2); printf("%10p=mp\n",mp); printf("gloval変数(初期値無し)\n"); printf("%10p=&s_var\n",&s_var); printf("%10p=&g_var\n",&g_var); fanc(0); printf("gloval変数(初期値有り)\n"); printf("%10p=&s_var3\n",&s_var3); printf("%10p=&g_var2\n",&g_var2); printf("文字列定数\n"); printf("%10p=pc2\n",pc2); printf("%10p=pc\n",pc); printf("%10p=c_var\n",&c_var); printf("関数\n"); printf("%10p=printf\n",printf); printf("%10p=main\n",main); printf("%10p=fanc\n",fanc); free(mp); }
実行結果
ローカル変数
0xbf8d1f98=&envs
0xbf8d1f94=&argv
0xbf8d1f90=&argc
0xbf8d1f74=&l_var
mallocしたもの
0x8288018=mp2
0x8288008=mp
gloval変数(初期値無し)
0x8049960=&s_var
0x8049964=&g_var
0x804995c=&s_var2
gloval変数(初期値有り)
0x8049954=&s_var3
0x8049950=&g_var2
文字列定数
0x80486fe=pc2
0x80486f6=pc
0x80486e4=c_var
関数
0x8048344=printf
0x8048435=main
0x8048418=fanc
となっており、
スタックセグメント | ←ローカル変数を格納 |
------------------------ |
------------------------ | |
ヒープセグメント | ←mallocなど動的に割り当てられる変数 |
------------------------ | |
BSSセグメント | ←初期化していないグローバル変数 |
------------------------ | |
データセグメント | ←初期化したグローバル変数、定数 |
------------------------ | |
テキストセグメント | ←関数など値不変のモノ |
mallocに関しては別に書く。
スタック
ストアドプログラム方式をノイマンが開発して依頼、
逐次実行はコンピュータの基本となった。
逐次実行にもFirst In First OutやLast In First Outなど種類がある。
見れば分かるように先に入れたのを先に出すか最後に入れたのを先に出すかの違いだが
どちらもバッファに処理をため込んで順次実行していくものである。
スタックとはLast In First Out(LIFO)型のバッファのことである。
(因みに前者のようなバッファをキューと呼ぶ。)
変数や関数が呼び出されたときのスタックの積まれる順番に付いて記述する。
void B(int b,int c) { int d=4; } void A(void) { int a=1; B(2,3); }
こんな関数があったとする。
当然関数A→関数Bという順にスタックが積まれるわけだが
関数の引数の方がローカル変数よりも先にスタックに積まれるのだが、ここでAの引数はvoidなので何も積まれない。
Aが呼び出されてからまずローカル変数aの1がスタックに積まれる。
続いてBの引数が積まれることになるのだがこのとき逆順に積まれる。1→3→2という感じ。(下図参照)
最後にdのローカル変数4が格納される。
スタックの構成
| | | | | | |4 |d |2 |b |3 |c |1 |a | |  ̄
ただしこれではdを使い終わったときにいつ戻ればいいのか分からない。
そこで戻りアドレスの情報もスタックに積まれる。
| | | | | | |4 |d |復帰情報 |2 |b |3 |c |1 |a | |  ̄
上の図を見れば分かるとおりdの下に復帰情報がある。
たとえばdのスタックをオーバーフローさせたらどうなるのか?
いわゆるクラッキングと呼ばれるものの殆どがこの復帰情報をすげ替えて自分の悪意あるコードを実行する。
これがすなわちスタックを足場にしたバッファオーバーフローというやり方である。
また、悪意あるコードとは大抵シェルを呼び出すだけの小さなコード、シェルコードと呼ばれるものである