目次
はじめに
こんにちは。
トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司(ktanonymous)です。
every Tech Blog Advent Calendar 2024(夏) の2日目の記事執筆担当者として参加させていただいております!
今回の記事では、普段書いているプログラムがCPUによってどのように理解されているのかについて、気になって勉強したのでまとめてみたいと思います。
(厳密には異なる表現があるかもしれませんが、概念的な理解を目指すものなので、ご容赦ください。)
CPUが理解できる言葉
CPUが理解できる言葉は、機械語と呼ばれるものです。
我々エンジニアが普段から書いているプログラミング言語は、CPUから見れば
「意味のわからない、ただの文字列でしかない」と言えるでしょう。
機械語とは、例えば、以下のように表現することができます。(CPUの種類によって表現が異なっていたり、そもそもの解読が辛かったりするので正確な表現・値ではありません)
01 00 10
各数値は16進数で表されており、これで「アドレス00
番地に値10
を書き込む」というように、処理(01
)と必要な対象を組み合わせて1つの命令を表現します。
では、プログラミング言語はどのように機械語としてCPUに解釈されるのでしょうか。
プログラミング言語が機械語として理解されるまで
アセンブリ言語
現在一般的に使われているプログラミング言語の話をする前に、アセンブリ言語について触れたいと思います。
アセンブリ言語とは、人間が理解しやすいように機械語と1対1で対応させた言語です。
アセンブリ言語がアセンブラと呼ばれるプログラムによって機械語に変換されることで、CPUが言語を理解できるようになります。
アセンブリ言語に関しては、実際にシェル上でobjdump
コマンド1を利用することで確認することができます(これはオブジェクトファイルの情報を表示するコマンドですが、-d
フラグをつけることで機械語を逆アセンブルすることができます)。
例として、筆者のマシン上でecho
命令を逆アセンブルしてみます。
$ objdump -d /bin/echo
すると、以下のような出力が得られます。 (なお、出力結果の全体は非常に長いので、先頭の数行を抜粋しています)
/bin/echo (architecture x86_64): (__TEXT,__text) section 100000bbc: 55 pushq %rbp 100000bbd: 48 89 e5 movq %rsp, %rbp 100000bc0: 41 57 pushq %r15 100000bc2: 41 56 pushq %r14 100000bc4: 41 55 pushq %r13 100000bc6: 41 54 pushq %r12 100000bc8: 53 pushq %rbx 100000bc9: 48 83 ec 28 subq $40, %rsp ...
左から、ファイル(今回は /bin/echo
)上でのオフセット、実際の機械語(16進数の値2~4つの組)、機械語に対応する命令を表すニーモニック(mnemonic)の順に並んでいます。
プログラミング言語の解釈
一般的に、我々が日々書いているソースコードは、アセンブリ言語への変換を目指して解釈が進められ、最終的にCPUが実行可能な機械語へと変換されます。
この解釈の過程を担っているのが、コンパイラやインタプリタと呼ばれるものになります。
コンパイル型言語やインタプリタ型言語というのは、この解釈の過程がどのように行われるかによって分類されます。
コンパイラ
コンパイラ は以下のように定義できます2 (説明のため一部表現を変えています)。
言語 のプログラムを言語 のプログラムに変換するプログラム
一般的なコンパイル型言語では、ソースコードをアセンブリ言語まで変換する役割をコンパイラが担っていることが多いでしょう(アセンブラはコンパイラに含まれている場合もあります)。
この定義から考えると、アセンブラもコンパイラの一種と言えると思いますが、
アセンブリ言語から機械語への変換は、変換元がアセンブリ言語であることを強調するためにアセンブラと呼ばれることがあります。
コンパイル型言語の1つとして、弊社でも利用されているGo言語が挙げられます。
Go言語では以下のステップを経てソースコードがコンパイルされます3。
- Parsing
- Lexical analysis (tokenize)
- Syntax analysis (parse)
- AST construction
- Type checking
- IR construction
- Middle end (最適化)
- Walk (順序評価、構文の低級化)
- Generic SSA(Static Single Assignment4)
- Generating machine code
Goのコンパイラにはアセンブラも含まれているため、最終的には機械語に変換されていることがわかります。
Goの標準のコンパイラである gc
はGoで実装されています。
これは、セルフホスティングと呼ばれる手法で、自身の言語で自身のコンパイラを書くというものです。
gcは元々C言語で書かれていましたが、この手法を用いることでGoで書かれたコンパイラが実現されています5。
自身の言語で実装されたコンパイラを実行するために、コンパイラのコード自身がコンパイルされている必要があります。
そのため、異なる言語で実装されたコンパイラ を用いて自身の言語で実装されたコンパイラ をコンパイルし、
最後に で 自身をコンパイルすることで、自身の言語で書かれたコンパイラが完成します(イメージ)。
また、コンパイラの実現方法の他に、コンパイルの手法にもJIT(Just-In-Time)コンパイラ6やAOT(Ahead-Of-Time)コンパイラ7などの種類があります。
TypeScriptからJavaScriptへの変換(トランスパイル)もコンパイルの一種です。
リンカ
一般的に、プログラムは複数のソースコードから構成されます。そのため、
コンパイラによって生成されたそれぞれのオブジェクトファイルは、そのままでは実行可能な1つのプログラムとはなりません。
これらのオブジェクトファイルをリンカと呼ばれるプログラムによって結合することで、実行可能な1つのプログラムが生成されます。
リンカは、関数のエントリーポイント情報を補完するなどして、複数のオブジェクトファイルを結合した1つの実行可能ファイルなどを生成します。
通常、ソースコードを機械語にコンパイルしてリンクするまでの過程を指して「ビルド」と呼びます。
なお、生成された実行可能ファイルは、ローダーによってストレージ(外部記憶装置)からメインメモリ(RAM)などに読み込まれます。
また、リンクには静的リンクと動的リンクの2種類があります。
静的リンクは、プログラムの実行に必要なライブラリなどを単一の実行ファイル内部にリンクする方法、
動的リンクは、呼び出される側のライブラリが実行時にリンクされる方法です。
リンク方法が静的か動的かどうかで実行ファイルのサイズや開発サイクルのスピードなどに違いが出てきます。
インタープリタ
インタープリタ は以下のように定義できます8 (説明のため一部表現を変えています)。
言語 を用いて実現した、言語 のプログラムが動作するプログラム
インタープリタは、実行時にソースコードを逐次解釈して実行するものです。
インタープリタの解釈手法には、ソースコードをそのまま逐次解釈するものもあれば、
一度バイトコードなどに変換(コンパイル)してから逐次解釈するものもあります。
例えば、Pythonの標準的なインタープリタであるCPython9は、
ソースコードをバイトコード(中間表現)に変換してから逐次解釈され、
PVM(Python Virtual Machine)によって処理されます10。
ちなみに、Pythonのバイトコードはdis
モジュール11を利用することで確認することもできます。
例えば、以下のようなコードを実行する場合を考えます(Google Colaboratoryでの実行を前提にしています)。
import dis def print_hello(): print("Hello!") dis.dis(print_hello)
このコードを実行すると、以下のような出力が得られます。
4 0 LOAD_GLOBAL 0 (print) 2 LOAD_CONST 1 ('Hello!') 4 CALL_FUNCTION 1 6 POP_TOP 8 LOAD_CONST 0 (None) 10 RETURN_VALUE
左から、ソースファイル内での行番号、命令のバイトコード(とその該当バイトインデックス)、 命令が取得する引数の参照インデックスと引数の順に並んでいます。
まとめ
今回の記事では、普段意識することのなかったプログラムの処理系について勉強したことをアウトプットしてみました。
これを知ったからといって普段のコーディングが劇的に変わるということはないと思いますが、
こういった基礎的な知識がシビアなシーンでは役に立つことも多いと思います。
この記事が、「なんかそれっぽいこと書いてるだけで勝手にPCが結果を出してくれる」を脱却したい人の一助になれば幸いです。
最後まで読んでいただき、ありがとうございました。
参考
- 大堀淳の計算機科学チャネル | コンパイラ ー原理と構造ー
- 筑波大学 | プログラミング言語処理 講義資料 | 言語処理系とは
- Rui Ueyama, 低レイヤを知りたい人のためのCコンパイラ作成入門, 2020/03/16
- 東京情報大学 | オペレーティング・システム | 第8回 プログラムの実行制御(その3) プログラムの実行
- 本当に初心者の人に捧げるコンピューター入門 | 1.4.3 まずは機械語
- wikipedia | 機械語
- Go コンパイラのコードを読んでみよう
- About the go command
- Introduction to the Go compiler
- GO | Frequently Asked Question
- logmi Tech | コンパイラが作ったバイナリをつなぎ合わせるプログラム「lld」の作者が語る、リンカの仕組み
- IT用語辞典 e-words | リンカ
- IT用語辞典 e-words | 静的リンク
- IT用語辞典 e-words | 動的リンク
- IT用語辞典 e-words | ローダー
- speakerdeck | Goコンパイラをゼロから作ってセルフホスト達成するまで / How I wrote a self hosted Go compiler from scratch
- what is a self-hosting compiler?
- wikipedia | インタープリタ
- CPython
- Python Glossary
- synopsys ブログ | Pythonバイトコードの知識
- objdump↩
- コンパイラ ー原理と構造ー 第2回:計算機の模倣、プログラミング言語の構造と原理、プログラミング言語開発の枠組み(25:32くらい)↩
- Introduction to the Go compiler↩
- Static Single Assignment(静的単一代入)↩
- what compiler technology is used to build the compilers?↩
- JIT コンパイラー↩
- AOT コンパイラー↩
- コンパイラ ー原理と構造ー 第2回:計算機の模倣、プログラミング言語の構造と原理、プログラミング言語開発の枠組み(21:25くらい)↩
- cpython↩
- bytecode↩
- disモジュール↩