タグ: アセンブリ言語

Cの基本的なコードとアセンブリコード

if文

絶対値を返す簡単な関数について見てみる。まずは最適化なし(-O0

cmp, jle, jmp 命令などによって if 文のふるまいが表現されていることがわかる。符号を反転させる部分は以下のような感じ(Swift)

続いて最適化あり(-O2

cmovl は conditonal move if less だそうで、つまり第1オペランドが第2オペランドより小さかったら第1オペランドへロードするというものらしい。だいぶ処理が軽くなっているのがわかる。

末尾呼び出しの最適化

末尾呼び出しでない階乗関数

Cのコードはこんなかんじで。

アセンブリコードを見てみる。

再帰が深い場合、スタックを食いつぶすことがわかる。続いて末尾再帰版。

最適化あり(-O1)で生成したコード。-O2 だとちょっと読むのが厳しいくらい最適化されたコードが生成されてしまったので…。

末尾呼び出しがジャンプ命令に置き換わっているのがわかる。コールスタックを消費しない形式となっている。

Hello, world

Cのコードを掲載するまでもないと思うんですが、まあ一応掲載

-O2 で生成したコード。

あんまり面白くなかった。”hello, work!” という文字列への参照を rdi レジスタにぶち込んで _puts よんでるだけだった。最後に xor eax, eax しているのは main 関数の return 0 を表現しているのだろう…。

参考文献

  • http://kira000.hatenadiary.jp/entry/2014/08/26/052447
  • https://codezine.jp/article/detail/485

関数呼び出しとアセンブリコード

関数呼び出しとアセンブリコード

次のような簡単な関数について見ていく。return_twotwice 関数を呼び出している。

こいつはどのようなアセンブリコードになるのだろうか。とりあえず clang -S -mllvm --x86-asm-syntax=intel call.c をしてみる。

_return_two はいつも通り、rbp レジスタの内容を退避させる。その後、twice 関数を呼び出しをおこなう。twice 関数は引数をひとつ取る関数で、どうやら edi レジスタに格納することにより、引数の引き渡しを実現しているようだ(mov edi, 1)。次に call 命令により _twice ラベルへと制御を引き渡している。

_twice でもいつも通り、rbp レジスタの内容を退避させる。それから、edi レジスタから引数を受け取り、ローカル変数と同じような感じで rbp-4へ格納する。その後の edi 領域を同じ値で上書きしている処理が見えるが、この意図はよく分からない。最適化をかけるとこういった処理はなくなるので一旦無視。次に edi レジスタの値を 1bit左シフトした値を、eax レジスタに格納して、この手続きは終了する。

最適化をかけた状態のアセンブリコードも見てみよう。clang -O2 -S -mllvm --x86-asm-syntax=intel call.c という感じでやってみると以下のような具合。

_return_two のほうに関しては、関数の戻り値が常に 2 になるために、もはや _twicecall すら走らない形に最適化されている。

_twice についてみてみると lea という命令が目につく。lea <src>, <dest> 命令は、scr のアドレスを計算し、dest にロードするというものです。lea はアドレス計算に使われるものではあるが、足し算を実現するのにも使われるようだ。64bit汎用レジスタ rdi の値を足し合わせて eax レジスタに格納している。なお、lea vs add についてはこの記事が詳しそう。

One significant difference between LEA and ADD on x86 CPUs is the execution unit which actually performs the instruction. Modern x86 CPUs are superscalar and have multiple execution units that operate in parallel, with the pipeline feeding them somewhat like round-robin (bar stalls). Thing is, LEA is processed by (one of) the unit(s) dealing with addressing (which happens at an early stage in the pipeline), while ADD goes to the ALU(s) (arithmetic / logical unit), and late in the pipeline. That means a superscalar x86 CPU can concurrently execute a LEA and an arithmetic/logical instruction.

複数の引数を取る関数

複数の引数を取る関数についても見ていこう。

このCコードはどのようになるだろうか。-O2 で最適化をかけたアセンブリコードを見てみる。

_add2 は引数が edi, esi に格納されて渡されるようだ。_add3edi, esi, edx から引数を受け取っている。

グローバル変数とローカル変数とアセンブリコード

次のようなグローバル変数がどのように扱われるかを確認する

clang -S -mllvm --x86-asm-syntax=intel global.c で以下のようなコードが生成される。

なるほど、グローバル変数 aDATA セクションにラベル _a が振られているようです。2倍の演算は shl によって左ビットシフトすることにより実現しているようです。

未定義のグローバル変数に関しては .comm という擬似命令を使って表現されるようです。次のCコードで確認してみます。

.comm についてリファレンスには以下のように記載してあります。

.comm name, size,alignment
The .comm directive allocates storage in the data section. The storage is referenced by the identifier name. Size is measured in bytes and must be a positive integer. Name cannot be predefined. Alignment is optional. If alignment is specified, the address of name is aligned to a multiple of alignment.

今回 add 関数は a1 を3倍した値を返すという内容にしてみました(名前変えるのわすれてた)。すると imul という命令が登場しました。こいつは単純なSigned Multiply を実現する命令です。dword ptr [rip + _a1] の値を 3倍して eax レジスタに格納するといったことをやっています。どうやら2のべき乗のときだけ shl 命令を使い、それ以外のときは imul 命令が使われる雰囲気がある。

ローカル変数

対してローカル変数はどう扱われるのか。簡単なCコードで確認してみる。

最適化をかけずにアセンブリコードを生成。

.cfiを省いたアセンブリコード。各行にコメントを付与した。_add で関数のおきまりの処理(push rbp, mov rbp, rsp)を行ったあと、rbp はスタックポインタと同じ位置を指しているはずだ。int b = 10; というコードはスタック領域に積む形で値 10 が格納されることにより実現されていることがわかる。ただし rsp は進んでいないため、スタックにプッシュしたことにはならない。すなわち、関数の外側からはローカル変数には(通常)アクセスできないような仕組みになっている。なるほど…という感じだ。あとは eax に演算処理結果を突っ込んでいって ret するという流れのようです。

ところで、このCコードにおけるローカル変数 b は明らかに無駄なコードです。最適化オプションをオンにして生成されるアセンブリコードを見てみます。

今度は、rbp-4 へ 10 の格納を行わず、直接 eax レジスタへ定数 10 を加算しているのが見て取れる。なるほどなぁ…って感想です。

参考文献

  • https://ja.wikibooks.org/wiki/X86%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9/GAS%E3%81%A7%E3%81%AE%E6%96%87%E6%B3%95
  • http://www.mztn.org/slasm/arm07.html
  • http://milkpot.sakura.ne.jp/note/x86.html
  • http://www7b.biglobe.ne.jp/~robe/pf/pf001.html
  • https://docs.oracle.com/cd/E26502_01/html/E28388/eoiyg.html
  • http://x86.renejeschke.de/html/file_module_x86_id_138.html
  • https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-39/index.html

定数を返すだけの関数のアセンブリコード

いろいろあって、必要に迫られアセンブラの勉強を始めた。基本的に知識が全くないので非常に低レベルな自分用のまとめです。

定数を返すだけの関数

とりあえず定数を返すだけの関数を定義してみる。

clang -S -mllvm --x86-asm-syntax=intel const.c で生成されたファイルが以下のような具合。

.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 ディレクティブを外してみていくのが良さそうなので、一旦削ったものを以下に示す。

(1) の push rbp では、rbp レジスタの内容をスタックに push している。(2) の mov rbp, rsp は、スタックポインタ rsp の値を rbp レジスタにセットしている。これらは何のために行なわれているのでしょうか。

関数の処理に入る前に rbp レジスタがどのように使われていたのかはわからないのですが、関数内ではこのレジスタを使います。ということは関数の処理を終える際に、rbp レジスタの値をもとに戻せないと困ります。もとに戻す処理は実際 (4) で行われています。

実際、pushpop の命令は次のようなものとおなじになります。

スタックポインタの値は push するとマイナス方向へ進み、pop するとプラス方向に進みます。

(2) ではスタックポインタ rsp の値を rbp に格納しています。これは mov 命令の [] を用いたアドレス指定に rsp レジスタを指定できない決まりになっているためらしいです。したがって、このように rbp に一旦移し、それを使って処理を記述していくことになります。

C言語では関数の戻り値を eax レジスタで返すことになっているため、(3) の処理では、eax に定数 1 を突っ込んでいます。(4) の処理で rbp の値をもとに戻して、 (5) の ret でスタックの値をみて制御を関数が呼ばれる前に記憶した位置に戻します。

【残ってる疑問】このケースの場合、rbp レジスタを利用していないので、(1)(2)(4)の処理は外せるのでは?

参考ページ

  • http://d.hatena.ne.jp/suu-g/20080510/1210408956
  • https://sourceware.org/binutils/docs-2.24/as/CFI-directives.html#CFI-directives
  • http://msumimz.hatenablog.com/entry/2014/02/19/214605