はじめに
- 今年のはじめに話題になった CPU 脆弱性の解説サイトを読んで最適化のための投機的実行について知り、本来は実行されるはずのない処理があらかじめ実行されるとかどういうこと?と驚き、低レイヤの世界ではよくわからないワンダーなことが行われているのだなあと興味を持ちました。
- この記事ではそんな低レイヤワンダーランドについて勉強してみたので、普段は高レイヤなアプリケーションを作っていて低レイヤは専門ではありませんよという自分と同じような人向けに入門的なことを雑に書いていきます。
- 取り扱うのは高水準言語からアセンブラ、機械語、プロセッサくらいまででソフトウェアの部分が主になります。
高水準言語はどうやって実行されるのか
- まずは高水準言語がコンパイルされて CPU で実行されるまでのざっくりとした流れを見てみます。
- 高水準言語としてはここでは C 言語を例にあげています。
- C 言語のコードはコンパイラによってコンパイルされ、アセンブリ言語のコードに変換されます。
- アセンブリ言語のコードはアセンブラによってアセンブリされて、機械語のコードに変換されます。1
- 機械語のコードはユーザーに実行されて OS によってメモリに読み込まれます。
- メモリ上の機械語のコードは CPU によって 1 命令ずつ読み込まれ、解釈され、オペランドの値を読み込み、命令が実行され、結果がメモリやレジスタに書き込まれ、次の命令の読み込みに戻ります。
- 詳細は後述していきます。
アセンブリ言語
アセンブリ言語とは
- 機械語を人間が読みやすいように翻訳した言語です。
- 基本的に機械語の命令とアセンブリ言語の命令が一対一で対応します。(マクロ的にひとつのアセンブリ言語命令が複数の機械語命令に翻訳されるものもあります。)
アセンブリ言語の例
- これは NASM というアセンブラ向けのアセンブリ言語で
Hello, World
を表示する例です。
hello.asm
section .data
message: db "Hello, World", 10
section .text
global _start
_start:
mov rax, 1 ; syscall number. 1 is write
mov rdi, 1 ; 1st argument. dest file descriptor number. 1 is stdout
mov rsi, message ; 2nd argument. input data start address
mov rdx, 13 ; 3rd argument. input data length
syscall
mov rax, 60 ; syscall number. 60 is exit
xor rdi, rdi ; 1st argument register
syscall
-
_start:
の行から処理が開始し、ひとつめのsyscall
でmessage:
の文字列を表示、ふたつめのsyscall
で exit します。 -
mov A, B
で B を A にセットするという意味で、rax
rdi
などはレジスタです。 - レジスタとは CPU 組み込みの記憶領域で、定義済みのグローバル変数のような感じで使われます。
-
syscall
は OS の機能を呼び出すコマンドで、事前に決まったレジスタに引数をセットしておいて実行すると write や exit などが呼び出されます。
機械語
機械語とは
- CPU に処理を実行させるためのプログラミング言語です。
- 機械語はバイナリデータなので人間が直接扱うのには向かず難しそうなイメージがありますが、利用する文字種がバイナリであるだけで、仕様に従って記載されたコードを読み込んで処理が実行されるという部分は他のプログラミング言語と同様です。
- 通常のプログラミング言語と大きく異なるのは CPU の種類毎に仕様が異なるという点です。CPU というハードの機能を直接実行するインターフェイスなのでハードの構成が異なると当然機械語の仕様も変わってきます。
どのような命令があるか
- 機械語の主な命令としては以下のようなものがあります。
- 四則演算・ビット演算・数値比較などの計算処理。
- goto や条件付き goto などによるフロー制御。
- メモリからデータを読む、メモリにデータを書く、入出力装置からデータを読む、入出力装置にデータを書き込む、などのデータ処理。
機械語の例
- これは書籍コンピュータシステムの理論と実装の 4 章に登場する機械語の例です。
- これは同書の 1 章から 3 章で作成される仮想 CPU 上で動作する機械語で、A レジスタに定数値を保存する命令と、A レジスタの値の位置にあるメモリのデータを D レジスタに保存する命令として解釈されます。
0000 0000 0000 0101 1111 1100 0001 0000
- 改行やコメントをつけると以下のようになります。
// A レジスタに定数値 "5" を保存する
// 命令は固定長 16 ビット
// A レジスタへのデータ保存命令は 1 ビット目が 0
0
// 続く 15 ビットはそのまま保存される値になる
000 0000 0000 0101
// A レジスタの値の位置にあるメモリのデータを D レジスタに保存する
// データの処理と代入命令は先頭 3 ビットが 1
111
// 続く 7 ビットが `1110000` だと `A レジスタの値の位置のメモリのデータを代入するデータの読み込み元` とする
1 1100 00
// 続く 3 ビットが `010` だと `代入先は D レジスタ` とする
01 0
// 続く 3 ビットが `000` なら `ジャンプしない`(代入する値が 0 なら A レジスタの値のアドレスへジャンプする、などの命令がある)
000
CPU は機械語をどうやって処理するか
- 図に示したとおり CPU が機械語を実行する大まかなフローは以下のとおりです。このあとそれぞれの項目について解説します。
- 命令の読み込み
- 命令を解釈
- 処理対象の読み込み
- 処理の実行
- 結果の書き込み
1. 命令の読み込み
- メモリに読み込まれた機械語のコードはメモリ上のアドレスを持ちます。
- コードの開始位置の命令のアドレスは CPU にある "実行命令アドレスを保存するためのレジスタ" に保存されます。
- CPU は実行命令アドレスレジスタから命令の位置を取得してひとつの命令を読み込みます。
2. 命令を解釈
- 読み込まれた命令のデータは機械語の仕様に従って解釈されます。
3. 処理対象の読み込み
- 計算処理などで必要であれば、レジスタやメモリからデータを読み込みます。
4. 処理の実行
- CPU の回路は信号によって組み換えができるようになっています。
- CPU は解釈した命令に合わせて回路を組み替えて、読み込んだデータを使って処理を行います。
5. 結果の書き込み
- 処理によって必要であれば、処理の結果をレジスタやメモリに保存します。
CPU はどうやってメモリとデータのやりとりをするか
- CPU とメモリの間は
コマンドバス
とアドレスバス
とデータバス
という 3 つの経路でつながっています。 - 書き込みに際には
コマンドバス
に書き込み信号を出しアドレスバス
に書き込み先アドレスを知らせデータバス
に書き込みたいデータを送ります。 - 読み込みに際には
コマンドバス
に読み込み信号を出しアドレスバス
に読み込み先アドレスを知らせるとデータバス
に読み込みたいデータが送られてきます。
CPU はどうやって入出力装置とデータのやりとりをするか
- 入出力装置とはディスプレイやキーボード、マウス、ハードディスク、プリンタ、LAN などデータの入出力を行う装置の総称です。
- 入出力装置とデータをやりとりする手段はいくつかあるので
I/O バス
と割り込み
についてふれます。
I/O バス
- 入出力装置は種類が多いので取扱を簡単にするために汎用的な規格が決められています。
- 現在一般的に用いられているのは PCI Express という I/O バスです。
- PCI Express では入出力装置毎にメモリ空間が割り当てられ、そのメモリに制御信号やデータ本体を読み書きすることで入出力装置とのデータのやりとりができる。
割り込み
- I/O バスを使ってデータのやりとりをする際に CPU が入出力装置への書き込みや読み込みが完了したかどうかを知りたい場合は、対応するメモリ空間を監視し続ける必要があるが、これは CPU の資源を無駄に利用してしまうのであまり好ましくない場合がある。
- そこで入出力装置側から読み込みや書き込みの終了を通知するための仕組みとして割り込みという仕組みがある。
- CPU には割り込み入力線というものが用意されており、入出力装置からイベントを通知したい場合はここに信号を送ることで CPU 側でイベントに対応した処理が実行される。
CPU はどうやって OS とやりとりをするか
- ユーザーのプログラムからメモリ割り当てやファイル操作などの OS の機能を使いたい場合は、入出力装置の項目で出てきた
割り込み
を使います。-
割り込み
は文脈によって例外
と呼ばれることもあります。
-
- 高水準言語で OS の機能を利用したコードは、機械語では割り込みを発信するコードに翻訳され、実行時には割り込みによって OS に処理が移譲されます。
低レイヤについてちゃんと勉強するための本の紹介
- 以上、低レイヤについて雑に説明してきましたが、ちゃんと勉強したい場合はちゃんとした本を読んだほうが良いだろうと思うので、自分が読んだ本の紹介をしていきます。
プロセッサを支える技術
- わかっている人がわかりやすくまとめてくれた本という感じの本です。
- 小学校の図書室とかに「〇〇の仕組み」といった本があったと思いますが、あれの大人向け版という感じで、タイトルの通りプロセッサのことはもちろん、コンピュータの基礎やメモリ、仮想化、GPU などについてやさしい言葉で丁寧に紹介してくれていて読みやすいです。
- 抽象度が高めなので、細かい所については実際はどうなっているのかわからなかったりしますが、わからないから知りたいというのがモチベーションの一つになりましたし、この本で大まかな概念について学んでおくことで、他の本でもっと細かい話をされて難しくてわからなくなったときにもこの本の説明を思い出して俯瞰して考え直すことで読み進めることができました。
低レベルプログラミング
- アセンブラと C 言語についてコードを書きながら入門していくような本です。
- アセンブラについてはちゃんと環境構築からはじめて実行方法を案内しながら教えてくれているので、何も知らないところから始めてもわかりやすく学習することができました。
- C 言語については基本的な文法の紹介などから始めていて初心者向けっぽくはあるのですが、環境構築やプログラムの実行など細かいところまでのフォローがなかったり、おそらく何らかの予備知識が必要なことを急に話し出してよくわからなくなったりするのでちょっと消化不良気味でした。
- とはいえ、この本を読むまでは C 言語のことを GC もない原始的な言語で絶対自分では書きたくないと思っていましたが、この本でアセンブラを学んでから C 言語を学ぶことで超パワフルで超頼りになるベターアセンブラなのだと認識し直すことができたのは面白かったです。
Linux のしくみ
- OS やハードウェアの基本的なしくみについての解説と、プログラムやコマンドを使ったしくみの検証をしてくれる本です。
- 図も説明もわかりやすいので OS やハードウェアについて知りたいときに最初に読む一冊としておすすめできそう。
- これから勉強したいという場合以外にも、仮想メモリのしくみについては知っているけど実際に物理メモリを獲得しに行く様子を見たことがないから見てみたいとか計測する方法を知らないから知りたい、とかなら楽しんで読めると思います。
コンピュータの構成と設計
- 世界中の大学がコンピュータ技術の基礎教育用に教科書採用するというバイブル的な本だそうで、著者名から「パタヘネ」と呼ばれています。
- コンピュータ技術の基礎的な内容を網羅的かつ詳細に取り扱い、センテンス毎に練習問題も豊富に用意されているのでまさに教科書といった感じです。
- 詳細な内容まで割とわかりやすく説明してくれるので理解が深まり楽しくはあるのですが、詳細なだけ説明の量も増えるのであまり興味ない内容だとだれがちになったので、他の簡単な本で概要を掴みながらこの本で興味のある部分の詳細を探っていくというような読み方が良さそうな気がします。
-
簡単のために省略していますが、ここでは実際はアセンブリ言語からオブジェクトファイルに変換して、リンカを使ってオブジェクトファイルから機械語の実行可能ファイルに変換します。 ↩