PythonコードからLLVM IR 、アセンブリへ可視化する
サマリ
PythonコードをLLVM IR、アセンブリへ可視化しました。
マシンスペック
MacBook Air M2 arm64
LLVMとは
The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name "LLVM" itself is not an acronym; it is the full name of the project.
LLVM began as a research project at the University of Illinois, with the goal of providing a modern, SSA-based compilation strategy capable of supporting both static and dynamic compilation of arbitrary programming languages. Since then, LLVM has grown to be an umbrella project consisting of a number of subprojects, many of which are being used in production by a wide variety of commercial and open source projects as well as being widely used in academic research. Code in the LLVM project is licensed under the "Apache 2.0 License with LLVM exceptions"
機械翻訳
LLVMプロジェクトは、モジュール式で再利用可能なコンパイラとツールチェーン技術の集合体である。その名前とは裏腹に、LLVMは従来の仮想マシンとはほとんど関係がない。LLVM」という名前自体は頭字語ではなく、プロジェクトの正式名称である。
LLVMは、任意のプログラミング言語の静的コンパイルと動的コンパイルの両方をサポートできる、最新のSSAベースのコンパイル戦略を提供することを目的とした、イリノイ大学の研究プロジェクトとして始まった。それ以来、LLVMは多くのサブプロジェクトからなる包括的なプロジェクトに成長し、その多くは学術研究だけでなく、さまざまな商用プロジェクトやオープンソースプロジェクトで実運用されている。LLVMプロジェクトのコードは、「Apache 2.0 License with LLVM exceptions 」の下でライセンスされている。
LLVMのインストール
brew install llvm
pip install llvmlite numba
# PATHの設定(必要に応じて)
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
Pythonコードの作成
def add(x, y):
return x + y
result = add(1, 2)
print(result)
LLVM IRへの変換
from numba import jit, types
@jit(nopython=True)
def add(x, y):
return x + y
add.compile("int64(int64, int64)")
llvm_ir_dict = add.inspect_llvm()
llvm_ir = llvm_ir_dict[(types.int64, types.int64)]
print(llvm_ir)
上記を実行します。
python llvm_ir.py > add.ll
LLVM IRからアセンブリへ変換
llc add.ll -o add.s
アセンブリコードの確認
head add.s
.build_version macos, 15, 0
.section __TEXT,__text,regular,pure_instructions
.globl __ZN8__main__3addB2v1B38c8tJTIeFIjxB2IKSgI4CrvQClQZ6FczSBAA_3dExx ; -- Begin function _ZN8__main__3addB2v1B38c8tJTIeFIjxB2IKSgI4CrvQClQZ6FczSBAA_3dExx
.p2align 2
__ZN8__main__3addB2v1B38c8tJTIeFIjxB2IKSgI4CrvQClQZ6FczSBAA_3dExx: ; @_ZN8__main__3addB2v1B38c8tJTIeFIjxB2IKSgI4CrvQClQZ6FczSBAA_3dExx
; %bb.0: ; %entry
mov x8, x0
add x9, x3, x2
mov w0, wzr
str x9, [x8]
add x9, x3, x2という行がありますが、これがLLVM IRのadd命令に対応するARM64のアセンブリ命令です。
「レジスタx3とx2の値を足し算し、結果をレジスタx9に格納せよ」という意味になります。
アセンブリ→機械語変換
# オブジェクトファイル生成
as add.s -o add.o
上記のコマンドで、add.oというオブジェクトファイルが生成されます。このファイルはバイナリデータなので、人間には読むことができないので
#include <stdio.h>
int main() {
long long result = 1 + 2;
printf("Result: %lld\n", result);
return 0;
}
cc main.c -o main
バイナリ解析
逆アセンブル
objdump -d add.o | head
add.o: file format mach-o arm64
Disassembly of section __TEXT,__text:
0000000000000000 <ltmp0>:
0: aa0003e8 mov x8, x0
4: 8b020069 add x9, x3, x2
8: 2a1f03e0 mov w0, wzr
c: f9000109 str x9, [x8]
シンボルテーブル
objdump -t add.o | head
add.o: file format mach-o arm64
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000150 l O __TEXT,__const _.const.add
0000000000000160 l O __TEXT,__const _.const.missing Environment: _ZN08NumbaEnv8__main__3addB2v1B38c8tJTIeFIjxB2IKSgI4CrvQClQZ6FczSBAA_3dExx
0000000000000150 l O __TEXT,__const ltmp1
00000000000001c0 l O __LD,__compact_unwind ltmp2
00000000000001e0 l O __TEXT,__eh_frame ltmp3
16進数で出力
hexdump -C add.o | head
00000000 cf fa ed fe 0c 00 00 01 00 00 00 00 01 00 00 00 |................|
00000010 05 00 00 00 18 02 00 00 00 20 00 00 00 00 00 00 |......... ......|
00000020 19 00 00 00 88 01 00 00 00 00 00 00 00 00 00 00 |................|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000040 20 02 00 00 00 00 00 00 38 02 00 00 00 00 00 00 | .......8.......|
00000050 20 02 00 00 00 00 00 00 07 00 00 00 07 00 00 00 | ...............|
00000060 04 00 00 00 00 00 00 00 5f 5f 74 65 78 74 00 00 |........__text..|
00000070 00 00 00 00 00 00 00 00 5f 5f 54 45 58 54 00 00 |........__TEXT..|
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000090 44 01 00 00 00 00 00 00 38 02 00 00 02 00 00 00 |D.......8.......|
Mach-Oヘッダ情報 (macOS)
otool -h add.o
add.o:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777228 0 0x00 1 5 536 0x00002000
ファイルの情報
file add.o
add.o: Mach-O 64-bit object arm64
size add.o
__TEXT __DATA __OBJC others dec hex
500 0 0 32 532 214
実行時間計測
time ./main
Result: 3
./main 0.00s user 0.00s system 2% cpu 0.254 total
まとめ
本記事では、PythonコードからLLVM IR、アセンブリ、機械語、オブジェクトファイルまで、実際に手を動かして変換・可視化し、その各段階でどのようなデータ構造や命令列になるかを観察しました。
Pythonの高水準な関数定義が、Numba/llvmliteによってLLVM IRに変換され、そこからアセンブリ(今回はARM64)へ、さらに機械語(オブジェクトファイル)へと落ちていく流れを確認しました。
objdumpやhexdump、otool等のツールを活用し、目に見えないプログラムの内部表現や実行形式を人間が観察できる形で提示しました。
この一連の流れを通じて、「プログラムはどのように解釈され、実行されるのか」を多層的に可視化しました。
Pythonは抽象度の高い言語ですが、最終的には機械語としてCPUに命令が渡されるという事実が、
実際のバイナリまで追いかけることができました。
Discussion