🔖

C言語の値渡し: M1 Macでaarch64アセンブリを眺める

2022/09/25に公開約3,000字

Cで関数を呼ぶ際、引数の値がレジスタや主記憶のレベルでどのようにやりとりされるかは呼出規約に依ります。

https://ja.m.wikipedia.org/wiki/呼出規約

そういえばApple SiliconのMacに引っ越したところで、せっかくなのでaarch64(arm64)のdarwinバイナリをディスアセンブルしてそこを観察してみます。

  • 環境
    • MacBook Pro (14-inch, 2021)、Apple M1 Max
    • macOS Monterey

Cのソースコード

// main.c
#include <stdio.h>

int add(int x, int y) {
  int sum = x + y;
  return sum;
}

int main(void) {
  int a = 33;
  int b = 4;
  int result = add(a, b);
  return 0;
}

Cをオブジェクトファイルにコンパイル

Cで書いた関数との対応を見やすくするために、debug infoをつけてオブジェクトファイルを出力してみます。

clang main.c -O0 -g -c -o main.o
  • -O0: 最適化レベル0
  • -g: debug infoをつける(後でディスアセンブルする時に見やすい)
  • -c: オブジェクトファイルを出力

すると

% file main.o
main.o: Mach-O 64-bit object arm64

オブジェクトファイル main.o が出力できました。

ディスアセンブル

objdumpでディスアセンブルします。

objdump -d -S main.o > main.o.dmp
  • -d: --disassemble
  • -S: debug infoを元にソースコードを挿入

main.o.dmp にダンプできました。

ディスアセンブル結果

% cat main.o.dmp

main.o:	file format mach-o arm64

Disassembly of section __TEXT,__text:

0000000000000000 <ltmp0>:
; int add(int x, int y) {
       0: ff 43 00 d1  	sub	sp, sp, #16
       4: e0 0f 00 b9  	str	w0, [sp, #12]
       8: e1 0b 00 b9  	str	w1, [sp, #8]
;   int sum = x + y;
       c: e8 0f 40 b9  	ldr	w8, [sp, #12]
      10: e9 0b 40 b9  	ldr	w9, [sp, #8]
      14: 08 01 09 0b  	add	w8, w8, w9
      18: e8 07 00 b9  	str	w8, [sp, #4]
;   return sum;
      1c: e0 07 40 b9  	ldr	w0, [sp, #4]
      20: ff 43 00 91  	add	sp, sp, #16
      24: c0 03 5f d6  	ret

0000000000000028 <_main>:
; int main(void) {
      28: ff c3 00 d1  	sub	sp, sp, #48
      2c: fd 7b 02 a9  	stp	x29, x30, [sp, #32]
      30: fd 83 00 91  	add	x29, sp, #32
      34: 08 00 80 52  	mov	w8, #0
      38: e8 0f 00 b9  	str	w8, [sp, #12]
      3c: bf c3 1f b8  	stur	wzr, [x29, #-4]
      40: 28 04 80 52  	mov	w8, #33
;   int a = 33;
      44: a8 83 1f b8  	stur	w8, [x29, #-8]
      48: 88 00 80 52  	mov	w8, #4
;   int b = 4;
      4c: a8 43 1f b8  	stur	w8, [x29, #-12]
;   int result = add(a, b);
      50: a0 83 5f b8  	ldur	w0, [x29, #-8]
      54: a1 43 5f b8  	ldur	w1, [x29, #-12]
      58: 00 00 00 94  	bl	0x58 <_main+0x30>
      5c: e8 03 00 aa  	mov	x8, x0
      60: e0 0f 40 b9  	ldr	w0, [sp, #12]
      64: e8 13 00 b9  	str	w8, [sp, #16]
;   return 0;
      68: fd 7b 42 a9  	ldp	x29, x30, [sp, #32]
      6c: ff c3 00 91  	add	sp, sp, #48
      70: c0 03 5f d6  	ret

Cのコードも挿入されていてとても見やすいです。アセンブリの復習をするのにちょうど良さそうです。

アセンブリを眺める

では早速中身を見てみます。

add 関数を呼ぶところを見てみると

;   int result = add(a, b);
      50: a0 83 5f b8  	ldur	w0, [x29, #-8]
      54: a1 43 5f b8  	ldur	w1, [x29, #-12]
      58: 00 00 00 94  	bl	0x58 <_main+0x30>

w0w1 レジスタに変数 ab の値をロードしてから add 関数に飛んでいます。

次に、add 関数の中を見てみると

; int add(int x, int y) {
       0: ff 43 00 d1  	sub	sp, sp, #16
       4: e0 0f 00 b9  	str	w0, [sp, #12]
       8: e1 0b 00 b9  	str	w1, [sp, #8]

w0w1 レジスタの値をストアしています。

結論

引数をレジスタにロードしておいて、関数の中でストアしているだけです。

アセンブリの題材としてはとても簡単ですが、他の行の命令にも目を通してみるとaarch64のRISCっぽさが感じられて面白いです。

Discussion

ログインするとコメントできます