いろいろあって、必要に迫られアセンブラの勉強を始めた。基本的に知識が全くないので非常に低レベルな自分用のまとめです。
とりあえず定数を返すだけの関数を定義してみる。
int one() { return 1; }
clang -S -mllvm --x86-asm-syntax=intel const.c
で生成されたファイルが以下のような具合。
.section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 11 .globl _one .align 4, 0x90 _one: ## @one .cfi_startproc ## BB#0: push rbp Ltmp0: .cfi_def_cfa_offset 16 Ltmp1: .cfi_offset rbp, -16 mov rbp, rsp Ltmp2: .cfi_def_cfa_register rbp mov eax, 1 pop rbp ret .cfi_endproc .subsections_via_symbols
.globl
はリンカに渡す名前を定義する擬似命令。.align 4, 0x90
については、4 の倍数のアドレスに配置してくれという命令になるようです。_one
は C 言語にもあるようなラベルで使い方もだいたい同じ。
cfi_startproc
は全然わからなかったので調べたらなんとなく説明があるページをみつけた。
.cfi_startproc is used at the beginning of each function that should have an entry in .eh_frame. It initializes some internal data structures. Don't forget to close the function by .cfi_endproc.
ふむ。とりあえず cfi
から始まる擬似命令は Call Frame Information とよばれるものに関する何かなようだ。よくわかっていない。stack overflow にそれらしき内容の質問があった。特定のプラットフォーム下では例外処理の際に Call Frame Information
を利用しているそうな。とりあえず cfi ディレクティブを外してみていくのが良さそうなので、一旦削ったものを以下に示す。
_one: ## @one push rbp ## (1) mov rbp, rsp ## (2) mov eax, 1 ## (3) pop rbp ## (4) ret ## (5)
(1) の push rbp
では、rbp
レジスタの内容をスタックに push している。(2) の mov rbp, rsp
は、スタックポインタ rsp
の値を rbp
レジスタにセットしている。これらは何のために行なわれているのでしょうか。
関数の処理に入る前に rbp
レジスタがどのように使われていたのかはわからないのですが、関数内ではこのレジスタを使います。ということは関数の処理を終える際に、rbp
レジスタの値をもとに戻せないと困ります。もとに戻す処理は実際 (4) で行われています。
実際、push
と pop
の命令は次のようなものとおなじになります。
# push <r32> mov [rsp-4], <r32> sub rsp, 4 # pop <r32> add rsp, 4 mov <r32>, [rsp-4]
スタックポインタの値は push するとマイナス方向へ進み、pop するとプラス方向に進みます。
(2) ではスタックポインタ rsp
の値を rbp
に格納しています。これは mov 命令の []
を用いたアドレス指定に rsp
レジスタを指定できない決まりになっているためらしいです。したがって、このように rbp
に一旦移し、それを使って処理を記述していくことになります。
C 言語では関数の戻り値を eax
レジスタで返すことになっているため、(3) の処理では、eax
に定数 1
を突っ込んでいます。(4) の処理で rbp
の値をもとに戻して、 (5) の ret でスタックの値をみて制御を関数が呼ばれる前に記憶した位置に戻します。
【残ってる疑問】このケースの場合、rbp
レジスタを利用していないので、(1)(2)(4)の処理は外せるのでは?
ウェブ界隈でエンジニアとして労働活動に励んでいる @gomi_ningen 個人のブログです