今回はいよいよアセンブラでの関数
ただし、
復帰位置の取得
「関数」の原理
「関数」
C/
復帰位置
(アドレス) を記録しておいて、 関数での処理が完了したなら、 復帰位置に制御遷移する
という手順を踏めば、
たとえば以下のような実装によって、
※ 呼び出し元
:
leal rp, %eax # 復帰位置 rp を eax に格納
jmp func1
rp: # 復帰位置(Return Position)
:
※ 呼び出し先
func1:
:
jmpl *%eax # eax 位置に復帰
呼び出し先func1
)
専用命令の使用
先の実装例では、jmp
を用いた関数呼び出しに先立って、
しかし、
関数から復帰する位置は、jmp
命令の次の命令位置以外にありえません。そして一般的なCPUであれば、
つまり、
期待に違わず、
Intel x86アーキテクチャの場合、call
"ret
"
※ 呼び出し元
:
call func1
:
※ 呼び出し先
func1:
:
ret
局所的情報の保持
局所変数領域の確保
個々の関数実行時における固有の変数領域、
局所変数領域は、
このような特徴を持つ局所変数領域を実現するために、
スタック構造を用いて各関数の局所的な情報を格納する領域を
まずは関数での処理の開始の際に、
- ESPの値を4
[3] だけ減算 - EBP
(Extended Base Pointer) の値をESPの位置に記録 - ESPをEBPの位置に移動
- 関数で必要となる局所変数領域の分だけESPを減算

この時、
スタックフレーム中に確保された局所変数領域へのデータの読み書きは、
たとえば、
void
func1(){
int index;
index = 1;
:
}
# 局所変数領域の確保
subl $4, %esp
# esp の移動
movl %ebp, (%esp)
# ebp の格納
movl %esp, %ebp
# ebp を esp 位置に移動
subl $4, %esp
# index 領域の分だけ esp を移動
# 局所変数へのアクセス
movl $1, -4(%ebp)
EBPと局所変数格納領域アドレスとの差分である"-4"を用いた即値付き間接アドレッシングで、
また、
- ESPの位置をEBPの位置に移動
- ESPの位置から以前のEBP値を取り出してEBPを復旧
- ESPの値を4だけ加算

アセンブラで実装するなら以下のようになりますpop
命令で実施しています)。
movl %ebp, %esp # esp を ebp 位置に移動
popl %ebp # ebp の復旧と、esp の移動
これにより、
ところで、
しかし、enter
命令、leave
命令が提供されています。これらを用いて書き換えたプログラムを以下に示します。
enter
/leave
での実装func1:
enter $4, $0
movl $1, -4(%ebp)
leave
ret
復帰先アドレスの保存
専用命令call
/ret
を使用したプログラム例では、
実は、call
命令はスタック上に復帰先アドレスを格納するため、
call
命令は復帰先アドレスをスタック領域にプッシュし、ret
命令はスタック領域からポップした復帰先アドレスをEIPレジスタに格納=制御遷移します。
低レイヤーから見た関数呼び出し
今回は、
冒頭でjmp
を使用しました。
一般的には専用の命令であるcall
およびret
を使うわけですが、jmp
での実現方法と変わりはありません。つまり
ある関数の途中から別の関数の先頭へと制御遷移する「関数呼び出し」も、ある関数の(論理的)末尾から別の関数の途中へと制御遷移する「呼び出し元復帰」も、どちらも制御遷移である
ということです。
「それがどうしたの?」
func2
.text
.align 4
func1:
ret
func2:
ret
.global entry_point
entry_point:
int3
call func1
.global end_of_program
end_of_program:
int3
上記のプログラムは見ての通り、func1
を呼び出すだけのプログラムです。
しかし、func1
からの復帰直前にスタックを操作することで
func2
(gdb) run .... 0x00401003 in entry_point () (gdb) disassemble func1 Dump of assembler code for function func1: 0x00401000 <func1+0>: ret End of assembler dump. (gdb) disassemble func2 Dump of assembler code for function func2: 0x00401001 <func2+0>: ret End of assembler dump. (gdb) stepi 0x00401000 in func1 () ※ ret 実行直前 (gdb) info register esp esp 0x22ff88 0x22ff88 (gdb) x/1x 0x22ff88 0x22ff88: 0x00401008 ※ esp 値を元に復帰位置格納先を確認 この時点では call の次の位置 (gdb) set var *0x22ff88=0x00401001 ※ 復帰位置格納先を func2 のアドレスで上書き (gdb) stepi ※ func1 の ret を実行 0x00401001 in func2 () ※ 呼び出し元へ復帰せず func2 が呼ばれる (gdb)
関数 func1
から戻るはずが、func2
を呼び出してしまいました。
この実行例では、
これは脆弱性攻撃手法の1つで、ret
命令による復帰先が、
アークインジェクションと同様にバッファオーバーラン
しかし、