低レイヤのお勉強

低レイヤを知りたい人のためのCコンパイラ作成入門

アセンブラ

Compiler Explorer
Cソースからアセンブリを出力してくれるサイト

.intel_syntax noprefix  ;アセンブリの文法指定.x86-64
.globl plus, main    ;プログラム全体のグローバルな関数であることを明示

plus:
  add rsi, rdi
  mov rax, rsi ;関数のリターンは、RAXレジスタに設定
  ret

main:
  mov rdi, 3
  mov rsi, 4
  call plus
  ret
  • 関数のリターンは、RAXレジスタに設定する
  • 関数の第一引数 RDIレジスタ、第二引数 RSIレジスタに入れる
  • call: 関数の呼び出し命令
    • callの次の命令のアドレスをスタックにプッシュ(戻ってくるアドレス)
    • callの引数としてアドレスにジャンプ
  • 四則演算: 第一引数のレジスタに結果を上書く
    • add: 足し算
    • sub: 引き算
    • imul: 符号あり掛け算
    • mul: 乗算。オペランドを1つとる
      • mul rdi => rax = rax rdi
    • idiv: idivは暗黙のうちにRDXとRAXを取って、それを合わせたものを128ビット整数とみなして、それを引数のレジスタの64ビットの値で割り、商をRAXに、余りをRDXにセットする
  • mov: 第一引数に第二引数をコピーする
    • code:mov dst [rax]
      • raxに入っているアドレスからメモリの値をロードして、dstに値をセットする
    • code:mov [dst] src
      • dstの値をアドレスとみなして、そのアドレスのメモリにsrcの値をストアする
      • ⇒[]でレジスタの値をアドレスとして、メモリの内容にアクセスしてる?
  • comp:
    「フラグレジスタ」に比較結果をセットする
    cmpの実体は、「フラグレジスタ」だけを更新する特殊なsub命令
    setxxxで「フラグレジスタ」から8bitレジスタに書き込む
    • setl: より小さい場合バイトを設定(>,<)
    • setle: より小さいか等しい場合バイトを設定(>=,<=)
    • setne: 等しくない場合バイトを設定(!=)
    • sete: 等しい場合バイトを設定(==)
  • ret: 呼出し元関数の実行を再開
    • スタック(RSP)からアドレスをpop
    • popしたアドレスにジャンプ
  • スタックポインタ: RSPレジスタをスタックポインタとして、RSPレジスタが指すメモリにアクセスできる
    • pop
    • push
  • cqo: 符号拡張。RAXに入っている64ビットの値を128ビットに伸ばしてRDXとRAXにセットする
  • ジャンプ: 今いる命令のアドレスから、指定されたアドレスの命令のアドレスに移動することです.

レジスタ

  • RAX
  • RDI
  • RSI
  • RSP
  • RBP
    • 「ベースレジスタ」。関数フレームの開始位置を指すレジスタ。
    • ここに入っている値を「ベースポインタ」と呼ぶ。
    • ベースポインタを利用することで、RSPにpushされているローカル変数や関数パラメータを、RBPからの相対位置で変数にアクセスできるようにしている。
    • What is the purpose of the RBP register in x86_64 assembler? stackoverrun
  • AL
    • ALはRAXの下位8ビットを指す別名レジスタ。RAXをAL経由で更新するときに上位56ビットは元の値のままになるので、RAX全体を0か1にセットしたい場合、上位56ビットはゼロクリアする必要があります。

スタックマシン

スタックマシンは、スタックをデータ保存領域として持っているコンピュータのことです。したがってスタックマシンでは「スタックにプッシュする」と「スタックからポップする」という2つの操作が基本操作になります。プッシュでは、スタックの一番上に新しい要素が積まれます。ポップでは、スタックの一番上から要素が取り除かれます。

スタックマシンにおける演算命令は、スタックトップの要素に作用します。例えばスタックマシンのADD命令は、スタックトップから2つ要素をポップしてきて、それらを加算し、その結果をスタックにプッシュします(x86-64命令との混同を避けるために、仮想スタックマシンの命令はすべて大文字で表記することにします)。別の言い方をすると、ADDは、スタックトップの2つの要素を、それらを足した結果の1つの要素で置き換える命令です。(movzb命令)

;スタックポインタを用いた加算
;ex) 1+2
push 1
push 2

pop rdi;2
pop rax;1
add rax rdi

push rax;3

スタック上の変数領域

Cでは、変数領域をスタック(RSP)上にもつ。
関数ごとにRSPでリターンアドレスと関数呼び出しで使われるメモリ領域を管理する。このメモリ領域のことを「関数フレーム」や「アクティベーションレコード」と呼ぶ。

関数gのリターンアドレス
ローカル変数a
ローカル変数b←RSP

ただし、上記ではRSPが変更(push/pop)されるたびに、ローカル変数へのアクセスがRSPからのオフセットで利用できない。そのため、RSPとは別に関数フレームの開始位置を指す「ベースレジスタ」を用意する。
このレジスタの値を「ベースポインタ」と呼ぶ。

関数gのリターンアドレス
関数gの呼び出し時典のRBP←RBP
ローカル変数 aRBPからの相対位置でアクセス可能
ローカル変数b←RSP

関数fを関数gで呼び出すと

関数gのリターンアドレス
関数gの呼び出し時典のRBP
ローカル変数a
ローカル変数b
関数fのリターンアドレス
関数fの呼び出し時典のRBP←RBP(関数gの呼び出し時点のRBPの位置を記録)
ローカル変数x
ローカル変数y←RSP

この関数呼び出し時のスタックの状態を作るアセンブリは下記のようになる。この定型の命令のことを「プロローグ」と呼ぶ。

push rbp   ;今のrbpをスタックにpush
mov rbp, rsp  ;rbpにpushしたrbpの位置をrbpに保持
sub rsp, 16   ;必要な分だけ関数フレームを取得(メモリ確保)

関数からリターンするときは、RBPを書き戻して、RSPがリターンアドレスを指している状態でret命令を呼ぶ。これを実現する下記のような定型の命令のことを「エピローグ」と呼ぶ。

mov rsp, rbp   ;rspをrbpの位置に移動
pop rbp  ;rbpを前の値に書き換える
ret   ;リターンアドレスの位置に移動

1. 初期位置

関数gのリターンアドレス
関数gの呼び出し時典のRBP
ローカル変数 a
ローカル変数 b
関数fのリターンアドレス
関数fの呼び出し時典のRBP←RBP
ローカル変数 x
ローカル変数 y←RSP

2. rspをrbpで書き換え

関数gのリターンアドレス
関数gの呼び出し時典のRBP
ローカル変数 a
ローカル変数 b
関数fのリターンアドレス
関数fの呼び出し時典のRBP←RBP, RSP

3. rbpにpop

関数gのリターンアドレス
関数gの呼び出し時典のRBP←RBP
ローカル変数 a
ローカル変数 b
関数fのリターンアドレス←RSP

4. retでジャンプ

関数gのリターンアドレス
関数gの呼び出し時典のRBP←RBP
ローカル変数 a
ローカル変数 b←RSP

スタックのアラインメント

メモリとXMM0系レジスタ間で値を転送する命令では、メモリ上の値が16バイト境界に配置されていることを求める。 コンパイラは、関数呼び出し時のスタックポインタが16バイト整列されていることを前提に命令を発行するため、16バイト整列するように調整する責任がある。