Rubyの加算を掘り下げてみる
サマリ
Rubyの加算について、個人で好きなように掘り下げてみました。
結局Cからアセンブリ、機械語などに行きつきました。
スペック
MacBook Air M2 arm64
Rubyのコード
Rubyで何気なく書いている下記のコード。実行すると簡単に答えが返ってきます。
a = 1
b = 2
ret = a + b
p ret
一見簡単に加算とコンソールへの結果出力ができますが、内部で何が起きているのでしょうか?
今回は、足し算について掘り下げます。
RubyのソースコードはGithub上に公開されているため、その内容を追いかけます。
CRubyはnumeric.cにrb_int_plusという関数があり、こちらが該当すると推測します。
内部では、サブルーチンとしてfix_plus、rb_big_plus、rb_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