📌

Rubyの加算を掘り下げてみる

に公開

サマリ

Rubyの加算について、個人で好きなように掘り下げてみました。
結局Cからアセンブリ、機械語などに行きつきました。

スペック

MacBook Air M2 arm64

Rubyのコード

Rubyで何気なく書いている下記のコード。実行すると簡単に答えが返ってきます。

a = 1
b = 2
ret = a + b
p ret

一見簡単に加算とコンソールへの結果出力ができますが、内部で何が起きているのでしょうか?
今回は、足し算について掘り下げます。
RubyのソースコードはGithub上に公開されているため、その内容を追いかけます。
CRubyはnumeric.crb_int_plusという関数があり、こちらが該当すると推測します。
内部では、サブルーチンとしてfix_plusrb_big_plusrb_num_coerce_binが使用されていました。

C言語の類似コードで実現

#include <stdio.h>
int main(void) {
    int a = 1;
    int b = 2;
    int ret = a + b;
    printf("%d\n", ret);
    return 0;
}

Rubyの中身とは整合性は取れていないですが、C言語で加算を書くと上記になります。

アセンブリレベルで確認

C言語はコンパイルをすると中間言語としてアセンブリ言語を生成するため、コンパイルオプションをつけてアセンブリ言語を出力します。

gcc -O0 -m64 -S add.c -o add.s
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 14, 0	sdk_version 14, 4
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #48
	.cfi_def_cfa_offset 48
	stp	x29, x30, [sp, #32]             ; 16-byte Folded Spill
	add	x29, sp, #32
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp, #12]                   ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	mov	w8, #1
	stur	w8, [x29, #-8]
	mov	w8, #2
	stur	w8, [x29, #-12]
	ldur	w8, [x29, #-8]
	ldur	w9, [x29, #-12]
	add	w8, w8, w9
	str	w8, [sp, #16]
	ldr	w9, [sp, #16]
                                        ; implicit-def: $x8
	mov	x8, x9
	mov	x9, sp
	str	x8, [x9]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldr	w0, [sp, #12]                   ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #32]             ; 16-byte Folded Reload
	add	sp, sp, #48
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"%d\n"

.subsections_via_symbols

バイナリを確認する

アセンブリ言語も人間が読み書きできる言語なので、さらに進みます。

gcc -O0 -m64 add.c -o add

こちらで出力されるファイルはバイナリ形式で実行ファイルにもなっています。
とはいえ、バイナリは一般的には理解が難しいので、まずはobjdumpで可視化します。

objdump -d add | head

add:	file format mach-o arm64

Disassembly of section __TEXT,__text:

0000000100003f34 <_main>:
100003f34: d100c3ff    	sub	sp, sp, #48
100003f38: a9027bfd    	stp	x29, x30, [sp, #32]
100003f3c: 910083fd    	add	x29, sp, #32
100003f40: 52800008    	mov	w8, #0

次に、xxdコマンドでダンプします。
簡単に確認すると下記のようになっています。
アドレス番地に対して、入っている値が16進数で表示されました。

xxd add | head
00000000: cffa edfe 0c00 0001 0000 0000 0200 0000  ................
00000010: 1100 0000 2004 0000 8500 2000 0000 0000  .... ..... .....
00000020: 1900 0000 4800 0000 5f5f 5041 4745 5a45  ....H...__PAGEZE
00000030: 524f 0000 0000 0000 0000 0000 0000 0000  RO..............
00000040: 0000 0000 0100 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 1900 0000 8801 0000  ................
00000070: 5f5f 5445 5854 0000 0000 0000 0000 0000  __TEXT..........
00000080: 0000 0000 0100 0000 0040 0000 0000 0000  .........@......
00000090: 0000 0000 0000 0000 0040 0000 0000 0000  .........@......

最初の行だけ例に挙げると下記のようになります。

オフセット Byte列 (little-endian) フィールド 意味
0x00 cf fa ed fe magic 0xFEEDFACF Mach-O 64 bit
0x04 0c 00 00 01 cputype 0x0100000C ARM64
0x08 00 00 00 00 cpusubtype 0x00000000 ALL (generic)
0x0C 02 00 00 00 filetype 0x2 MH_EXECUTE

バイナリで加算がどうなっているかを確認する

objdump -d add | grep -n -E '\badd\b'

下記が出力されました。

2:add:	file format mach-o arm64
9:100003f3c: 910083fd    	add	x29, sp, #32
19:100003f64: 0b090108    	add	w8, w8, w9
26:100003f80: 913e9000    	add	x0, x0, #4004
30:100003f90: 9100c3ff    	add	sp, sp, #48

これの一部をxxdで該当箇所を確認します。

xxd add | grep  '00003f80'
00003f80: 0090 3e91 0500 0094 e00f 40b9 fd7b 42a9  ..>.......@..{B.

ARM64はリトルエンディアンなので、該当の箇所を見ると

バイト列 (LE) 32 bit 値 (hex) 意味
00 90 3e 91 0x913e9000 ADD X0, X0, #0xFA4

となります。
16進数0x913e9000は2進数で0b10010001001111101001000000000000となります。

まとめ

今回は、Rubyの加算命令の少しだけ深掘りとC言語を2進数まで落としてみました。

Discussion