🦩

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