x86-64とスタック
サンプルプログラムを作成して、x86-64のスタックの動作を確認した。
サンプルプログラムと実行結果
code:stack_test.S
// スタックのテストプログラム
// main関数
.intel_syntax noprefix
.globl main
main:
//=== 関数プレリュード ===
push rbp
mov rbp, rsp
//=== メイン処理 ===
// スタックポインタの値を印字
mov rdi, rsp
call print_hex
// rbp より上位のアドレスをダンプ
mov rdi, rbp // 開始アドレス
mov rsi, 64 // ダンプするサイズ
call dump
// 0x12345678 をスタックに積む
push 0x12345678
// スタックポインタの値を印字
mov rdi, rsp
call print_hex
// ダンプ
mov rdi, rbp // 開始アドレス
mov rsi, 64 // ダンプするサイズ
call dump
// 0xabcdef をスタックに積む
push 0xabcdef
// スタックポインタの値を印字
mov rdi, rsp
call print_hex
// ダンプ
mov rdi, rbp // 開始アドレス
mov rsi, 64 // ダンプするサイズ
call dump
// 積んだ値を破棄
pop rax
pop rax
//=== 関数エピローグ ===
pop rbp
ret
code:mylib.c
// long の値を16進数で表示
void print_hex(long n) {
printf("%lx\n", n);
}
// メモリのダンプ
// 以下のような感じに、8バイトづつ、デクリメントしながら表示
//
// 4000800a80: 12 34 56 78 9a bc de f0
// 4000800a78: 12 34 56 78 9a bc de f0
// 4000800a70: 12 34 56 78 9a bc de f0
void dump(long start_address, long size) {
for (long i = 0; i < size; i += 8) {
printf("%016lx: ", start_address - i);
for (int j = 0; j < 8; j++) {
printf("%02x ", *(unsigned char *)(start_address - i + j));
}
printf("\n");
}
}
ビルドと実行結果
code:sh
$ gcc -z noexecstack stack_test.S mylib.c && ./a.out
4000800a80
0000004000800a80: 01 00 00 00 00 00 00 00
0000004000800a78: 51 11 40 00 00 00 00 00
0000004000800a70: 80 0a 80 00 40 00 00 00
0000004000800a68: 18 00 00 00 00 00 00 00
0000004000800a60: 00 00 00 00 04 00 00 00
0000004000800a58: 80 0a 80 00 40 00 00 00
0000004000800a50: 40 00 00 00 00 00 00 00
0000004000800a48: e6 12 40 00 00 00 00 00
4000800a78
0000004000800a80: 01 00 00 00 00 00 00 00
0000004000800a78: 78 56 34 12 00 00 00 00
0000004000800a70: 6d 11 40 00 00 00 00 00
0000004000800a68: 80 0a 80 00 40 00 00 00
0000004000800a60: 20 00 00 00 00 00 00 00
0000004000800a58: 80 0a 80 00 04 00 00 00
0000004000800a50: 80 0a 80 00 40 00 00 00
0000004000800a48: 40 00 00 00 00 00 00 00
4000800a70
0000004000800a80: 01 00 00 00 00 00 00 00
0000004000800a78: 78 56 34 12 00 00 00 00
0000004000800a70: ef cd ab 00 00 00 00 00
0000004000800a68: 89 11 40 00 00 00 00 00
0000004000800a60: 80 0a 80 00 40 00 00 00
0000004000800a58: 28 00 00 00 00 00 00 00
0000004000800a50: 80 0a 80 00 04 00 00 00
0000004000800a48: 80 0a 80 00 40 00 00 00
考察
起動直後のスタックポインタの値とメモリのダンプを確認。
スタックポインタの値は「4000800a80」
スタック近辺のメモリには、以前使われたと思われる値がセットされている
code:dump
4000800a80 #=> (スタックポインタの値) (メモリのダンプ ... 以前使われたと思われる値がセットされている)
0000004000800a80: 01 00 00 00 00 00 00 00
0000004000800a78: 51 11 40 00 00 00 00 00
0000004000800a70: 80 0a 80 00 40 00 00 00
0000004000800a68: 18 00 00 00 00 00 00 00
0000004000800a60: 00 00 00 00 04 00 00 00
0000004000800a58: 80 0a 80 00 40 00 00 00
0000004000800a50: 40 00 00 00 00 00 00 00
0000004000800a48: e6 12 40 00 00 00 00 00
0x12345678 をスタックへプッシュ。
code:asm
// 0x12345678 をスタックに積む
push 0x12345678
元のスタックポインタのアドレス 0x4000800a80 から 64ビット(8バイト)分引いた値 0x4000800a78 が新しいスタックポインタのアドレスとなり、メモリ上のアドレス0x4000800a78 に 0x12345678 が格納される。
スタックポインタの値
0x4000800a80 - 64ビット(8バイト) = 0x4000800a78
メモリダンプ
アドレス 0x4000800a78 に 0x12345678 が格納される
0000004000800a78: 78 56 34 12 00 00 00 00
code:dump
4000800a78 #=> (スタックポインタの値) (アドレス 0x4000800a78 に、値 0x12345678 が格納されている)
0000004000800a80: 01 00 00 00 00 00 00 00
0000004000800a78: 78 56 34 12 00 00 00 00
0000004000800a70: 6d 11 40 00 00 00 00 00
0000004000800a68: 80 0a 80 00 40 00 00 00
0000004000800a60: 20 00 00 00 00 00 00 00
0000004000800a58: 80 0a 80 00 04 00 00 00
0000004000800a50: 80 0a 80 00 40 00 00 00
0000004000800a48: 40 00 00 00 00 00 00 00
次に、0xabcdef をスタックへプッシュ。
code:asm
// 0xabcdef をスタックに積む
push 0xabcdef
新しいスタックポインタとスタックの値は以下のとおり。0xabcdef がスタックへ格納される。
スタックポインタの値
0x4000800a78 → 0x4000800a70 (マイナス8バイト)
スタックの値
0000004000800a70: ef cd ab 00 00 00 00 00
まとめ
スタックポインタ rsp は、一番上の要素のアドレスを指す
スタックへ要素を積むと、64ビット(8バイト)分 rsp がデクリメントされる
以下のコードでスタックの一番上の要素を印字する
code:asm
// スタックの最後の要素を印字
call print_hex