実践的低レベルプログラミング

はじめに

学校で習わないが(習う学校もある)、現実に必要になるプログラミング技術に、低レベルプログラミングなどと呼ばれるものがある

厳密な定義は聞いたことがないし、おそらく存在しないとは思うが、大体のみんなの共通認識として、 「高級プログラミング言語を使わないプログラムを書き、OSで抽象化されないデバイスの機能を使う」といったような認識があると思う。

筆者の経験から言わせてもらうならば、低レベルプログラミングに関する知識は、プログラミングにおいてあらゆる場面で、常に、少しずつ役立てられる知識だと言えると思う。

普段はRubyやPHPなどを書いてる人であったとしても、メモリが足りなくなった場合や、デバッガを使っている場合、性能が足りなくなった場合など、 厳しい環境におかれた時に低レベルプログラミングに関する知識が必ず役に立つ場面が来ると信じている。

また、役に立つかどうかは置いておいても、「プログラムはどのように動いているのか」を知ることは、知的好奇心を満たし、 その知識を駆使すれば、コンピュータの挙動を手中に収めた全能感を楽しむことができることだろう。

多くのプログラム初心者が、 「#include <stdio.h> ってなになのか?」「何故 print "Hello World" というプログラムを実行すると、画面に"Hello World"と出るのか?」 と、いった点について、多かれ少なかれ疑問を持ちながら、モヤモヤとした気持ちでプログラムを書いたことがあるはずだ。

しかし、残念ながら、これらに関する知識は体系立てられているとは言えず、 また、低レベルプログラミングがターゲットとする領域では、 アドホックな方法で、場当たり的に実装されたものがそのままデファクトスタンダードになってしまう例が数多くあり、 なんらかの一般的な理論を勉強するよりは、実装ごとの事情を勉強する必要があるのが現実である。

それでも、現実にある多くの事情、デバイス、実装などを眺めていれば、そこはかとなく存在する一般的な概念を読みとることができるはずで、 それらの多くを実際に経験した筆者は、おそらく多くの人より、なんらかの普遍的な知識を獲得しているだろうという自信はある。

この文章は、筆者が書ける限りの、色々なデバイスの色々な事情について、書けるかぎり書いていこうというものである。 普遍的な知識を表現することは難しいが、色々なレイヤの事情を通して、なんらかの共通する概念を習得する人がひとりでも増えれば幸いである。

この文章の状態について

2018/10 現在、書きかけの状態です。とりあえず今は読みやすさよりも情報量増やすほうに注力しているので、読みやすさはあとまわしになっています。 ひとくぎり付いたら、推敲したり、体裁整えたり、図を入れたりしようと思います。

更新状況は https://github.com/tanakamura/pllp/tree/gh-pages から確認ください。 また @tanakmura で何か追加で言い訳などを書いている場合もあります。

気が向いたらリアルタイム更新を 配信 しています。

環境

なんらかの方法で Linux、GCC、binutils の環境を用意することを強くお勧めする。実機でもVMでもWSLでも構わない。(もしかすると実機でしかできないこともやるかもしれないが)

Windowsにも、優れた開発環境はあるが、少し道を踏みはずした低レベルプログラミングをする場合、ぱぱっと色々なコマンドを組みあわせて変なことができる Linux環境のほうがかなり使いやすい。道をはずしたプログラミングをするなら、Linux環境に慣れておくにこしたことはないと思う。

また、場合によってはARMなどの、PC以外の環境にも触れたいと思っているが、Linuxならば、その場合にもインターフェースが共通して使えるので、心強い。

巨大なライブラリなどは必要ない。C言語で書いた Hello World がコンパイルできる程度の環境があれば十分である。ついでにgdbも使えるようになっているとよい。

PC以外に、Raspberry Pi、Zybo などを使っていくかもしれない。手元にある人は使ってみてほしい。

アセンブリ、C言語、リンカ、ローダ、機械語、ABI

アセンブリ言語はご存知だろうか?ご存知のかたは、どの程度ご存知であろうか。 もし、あなたが、共有ライブラリロード時のリロケーションの処理が何なのか説明できる程度に、色々なことを知っているならば、もう、この章は飛ばしてもらってかまわない。(というかそもそもこの文章読む意味あるか?)

低レベルプログラミングにおいて、機械語は第一級言語だと言っていい。機械語に関する理解なく、低レベルプログラミングにチャレンジするのは、筆や鉛筆等を持たないで絵を描くのと似たようなものだ。

この章の目的は

の二点だ。

できる限り、わかりやすい説明を試みるが、かなり重いテーマなので、筆者の説明不足などにより、全てを正しく理解するのは難しいかもしれない。 その場合でも、次からの章を断片的に理解することはできるので、よくわからなければ、次の章へ進んでもらってもかまわない。 この文章全体を読み終えるくらいに、第一章が理解できるぐらいのペースでもよいと思う。

また、C言語への理解がまだ浅い人は、別のC言語の書籍、解説ページを用意して、そちらと交互に読んでいくものよいかもしれない。 ポインタなどがわからないという人も、その背後にある機械語を理解すれば、いくらか理解しやすくなることもある。C言語の文法などは基本的に説明しないので、 よくわからなければ、C言語の本に戻っていただいてもかまわない。多分、両方をちょっとずつ理解していくのがいいと思う。

x86_64 プログラミング入門 (一旦終了)

x86_64 機械語入門 (書きかけ)

Intel マニュアルの読みかた

ARMv7 プログラミング入門

リンカ (書きかけ)

C言語とアセンブリ

ABI, Calling Convention (呼び出し規約)

システムコール(OS呼び出し)

libc 自作

インタプリタ/コンパイラ

binutils

ここまで as,readelf,objdump などのツールを使ってきたが、これらのツールは、Linux では、 binutils と呼ばれるパッケージに含まれている。

binutils には、有用なツールが含まれているので、そのうちいくつかを簡単に紹介しておこう。

objdump

readelf

c++filt

addr2line

objcopy

おまけ : shellcode,PIC,PIE,ASLR

バス、メモリ、周辺IO、MMIO

Linux デバイスドライバ

低レベルプログラミングを習得するためにLinux デバイスドライバについて学習するのは良い方法かもしれない。

普段は、様々な問題からプログラマを守ってくれるOSではあるが、 道を外したプログラミングをする場合、このOSの保護が邪魔になる場合がある。

OSの保護を回避する手段として、「OSなし(ベアメタル)プログラミング」という世界がある。 これは、非常に楽しいプログラミングではあるが、場合によってはprintfで数値を画面に出力したり、mallocでメモリを確保するだけでも かなりの苦労を伴う手法である。

そこで、別の方法として、Linuxデバイスドライバを書くという方法もある。

Linuxデバイスドライバまわりの開発環境は、非常によく整備されていて、 printf ぐらい気軽に文字列を出力できるし、mallocぐらい気軽にメモリを割り当てられるし、 プログラムにミスがあってエラーが出れば、エラーが発生した箇所を教えてくれる機能が付いている。 「Linuxデバイスドライバ」という、優れた書籍があるのも嬉しい点だ。

また、Linuxデバイスドライバについて深く学ぶと、演習で使うようなtoy OSにはない、 現実世界で広く使われるOSに必要なものを肌で感じられるようになるというメリットもある (例えば、toyOSではCPUの速度に迫るような高速な周辺デバイスのことは考えられていないなど)。 あと現実的な話をしてしまうと、Linuxドライバを書いてほしい/修正してほしいという仕事は世の中にたくさんあり、 Linuxドライバが書けるようになっていると、職にあぶれないという点も見逃せない。

この章では、本文書の説明で使える程度の範囲内で、Linuxデバイスドライバの書きかたについて説明していく。 より詳しい使いかたに興味がある人は、書籍や、Linuxのソースコードを参照してほしい。

GPIOデバイスドライバ (みんな大好きLチカプログラミング)

UART による通信

ベアメタルプログラミング

低レベルプログラミングに興味がある人なら、OS自作や、ベアメタルプログラミングに手を出したことがある人も多いのではないだろうか。

しかし、現在のPCのマザーボードには、昔のOSに匹敵するような巨大なソフトウェアが搭載されているのが普通で、 もっと小さなハードウェアで実行されるベアメタルプログラムと比較すると、まだ厚いレイヤーの上で動くソフトウェアしか作れないという問題がある。 PCベアメタルプログラミングに手を出したことがある人も、BIOSコールを使って、HDDにアクセスしたり、 UEFIサービスを使って、ファイルシステムにアクセスするとき、「これは何か求めていたものとは違うのではないか?」と思いながらプログラムを書いていたはずだ。

ここでは、PCよりもレイヤーの薄いZyboを使って、ベアメタルプログラミングにチャレンジしていこうと思う。 (ちなみにRaspberry Piも、大きめのファームウェアを持っていて、ARMがブートするのは、色々な初期化が終わったあとだ)

ブートとは

SPI flash

クロック

DRAMコントローラ

ファームウェア、BIOS、UEFI

おまけ : デバイスツリー

OSを支える技術

メモリ保護

仮想化

割り込み

SMP

デバッガ

DMA

キャッシュ

メモリ

高速デバイスの世界

かつて、コンピュータというのは、高速なCPUと、中速なメモリ、低速なI/O から構成されていた。

しかし、近年は、CPUの速度向上がかなり停滞してきているのに対し、 周辺I/Oデバイスの速度向上は止まるどころか、高速なデバイスが次々に投入され、 その性能は、CPUに迫るか、それを大きく上まわるものも登場してきた。

遅いと言われていたストレージは、家庭用の物でもusec単位で処理が実行されており、 100GbpsイーサやInfinibandの帯域はCPUのmemsetに迫るぐらいになり、 GPUの演算スループット性能およびメモリ帯域はCPUの何倍も大きくなってきている。

ここでは、現代の高速デバイスが、どのように接続され、ユーザからどのように使うのかといった点について解説していきたいと思う。

PCI Express

イーサネット

Infiniband

USB

GPU

ストレージ

SATA, AHCI

NVMe

ネットワーク

TCP/IP

ファイルシステム

i2c

FPGA

RTC

ヒープ、malloc、GC

チューニング

低レベルプログラミングに関する書籍