本書を手に取っていただきありがとうございます。本書はC言語でもっとも重要な概念のひとつであるポインタをアセンブリを用いて理解を深めることを目的としています。
C言語を習得するにあたって、ポインタの概念が理解できない人が多いように見受けられます。多くの場合、その理由はアドレスやポインタの概念の理解が浅く、内部でどのようにデータが処理されているのかを理解できていないことによります。C言語で書かれたすべての処理はアセンブリに変換することができるため、アセンブリで書かれた処理を理解することでC言語でどのように処理されているのかがわかるようになります。本書ではC言語で書かれたポインタのコードをアセンブリに変換し、そのアセンブリコードを理解することでポインタの処理の理解を深めます。
本書の目的はポインタを理解するだけではありません。ハードウェアや低レイヤの未経験者が低レイヤについて学習したいと思っても、何から手をつけてよいかわからないと思います(実際に筆者もそうでした)。そこで本書は、そんな低レイヤ未経験者が低レイヤの世界に入門するための"門"となるような本としました。なので本書はあくまでポインタを理解するということが第一の目的ではありますが、第1章を「前提知識」と題して、コンピュータアーキテクチャの知識を解説し、次章からその知識を用いて読者が実際に手を動かして環境構築から順に行うため、ポインタの理解を深めるだけではなく低レイヤの技術にも入門することができるようになっています。なお本書で扱う命令セットはRISC-Vなので、RISC-Vに興味がある方にも参考にしていただきたいです。
本書を読んでわかることは以下になります。
・コンピュータの基礎知識
・C言語とアセンブリの関係
・ポインタを含むC言語の文法の内部的な処理
・RISC-Vアセンブリを読むための最低限の知識
本書では以下の人を対象読者としています。
・C言語のポインタを理解したい人
・低レイヤの技術に入門したい人
本書を読むにあたり、次のような知識が必要となります。
・C言語で書かれたソースコードを読むことができる
・Linuxの基本的なコマンド操作ができる
・gitの基本的なコマンド操作ができる
・dockerの基本的なコマンド操作ができる
本書は以下の環境を想定としています。
・gitがインストールされていること
・dockerがインストールされていること
アセンブリ言語を読めるようになるには、コンピュータアーキテクチャの知識を身につけることが必須です。この章では、アセンブリ言語を理解するためのコンピュータの知識や命令セットについて述べていきます。
コンピュータを構成するにあたって5つの重要な要素があります。図1.1に図を示します。
コンピュータは、大きく分けて「演算」「制御」「記憶」「入力」「出力」の5つの機能を持ちます。それぞれ「演算」は演算装置、「制御」は制御装置、「記憶」は記憶装置、「入力」は入力装置、「出力」は出力装置で行われます。演算装置と制御装置のふたつは、合わせて中央演算処理装置(一般的にはCPU、プロセッサと呼ばれる)内で処理されます。
入力装置はデータや命令などを記憶装置に入力する装置のことです。キーボードやマウス、スキャナーなどが入力装置です。
記憶装置(メモリ)はデータや命令(プログラム)を記憶する装置です。メモリには大きく分けて、電源を切ったら記憶内容が失われる特徴を持ち処理実行時に使用されるRAM、電源を切っても記憶内容が失われない特徴を持つ長期記憶用のROMの2種類があります。また記憶装置にはメモリの他にレジスタがあり、メモリはCPUの外部、レジスタはCPUの内部に存在し、アクセス速度はレジスタの方が速いです。
CPU内には演算装置と制御装置があります。制御装置は命令(プログラム)を解読(デコード)して他の装置の制御を行います。演算装置は制御装置が解読した命令に従って演算(処理)をします。
出力装置は処理結果を出力する装置です。モニターやプリンターなどが出力装置です。
ここではより詳しく記憶装置であるメモリとCPUの関係、どのように命令が実行されているのか解説します。なおここで紹介する流れはRISCアーキテクチャに基づいたものになります(RISCとCISCについては後述する)。
図1.2はメモリとCPUの関係図です。
命令は4つの手順で実行されます。表1.1にまとめました。また表1.1に記載されているプログラムカウンタとは次に実行する命令を指しているレジスタ、算術論理演算器(ALU:Arithmetic Logic Unit)とは実際に演算を行う装置、レジスタファイルとは汎用レジスタを多数集積したものです。なお実装によって様々な実行方法があるため、必ずこうでなければならないというわけではありません。
手順 | 処理名 | 処理内容 |
1 | 命令フェッチ | プログラムカウンタが指すメモリのアドレスから命令を取り出す(フェッチする)。 |
2 | 命令デコード | フェッチしてきた命令の命令コードを制御装置で解読(デコード)する。オペランドはレジスタファイルからレジスタを読み出す。 |
3 | 命令の実行 | オペランドを入力して算術論理演算器(ALU)で命令を実行する。 |
4 | 演算結果の格納 | 実行結果を汎用レジスタに格納する。 |
C言語で書かれたソースコードを動作させるには、ソースコードをコンパイラでコンパイルして実行ファイルを作成します。ここではC言語で書かれたソースコードが実行ファイルになるまでの工程を説明します。これを理解することで本書ではどの工程を扱うのかがわかり、理解がより深まると思います。図1.3にそのフローを示します。またコンパイラと聞くとgccやclangなどを想像する人がいると思いますが、これらは正確にはコンパイラドライバといいます。真の意味でのコンパイラはC言語のソースコードをアセンブリにコンパイルするツールのことを指し、コンパイラドライバはC言語のソースコードから一気に実行形式に変換をするツールのことを指します。つまりコンパイラドライバは図1.3の一連の流れを一気に実行してくれるツールのことです。
初めにC言語のソースコードはプリプロセッサによって"#include"などのファイルの読み込みや"#define"などの定数やマクロの展開をします。次にコンパイラによってコンパイルを行い、アセンブリファイルになります。そのアセンブリファイルをアセンブラによってアセンブルすることで0と1の情報(機械語)のオブジェクトファイルになります。最後にライブラリや他のオブジェクトファイルなどとリンカによってリンクすることで実行ファイルとなります。
図1.3に示していますが、本書ではソースコードからアセンブリファイルへコンパイルをする工程を扱います。
C言語にはグローバル変数や関数、動的に領域を確保するmallocなどいろいろな概念があります。それらはすべてメモリ上に領域が確保されて、その領域を変数や関数として定義します。C言語では、メモリ上に領域を確保する際に何をどこに展開するか決まっています。図1.4にC言語のメモリ領域を示します。
図1.4のメモリ図は、上に行くにつれてアドレス番地が小さくなり、下に行くにつれて大きくなります(本書の図はすべてこのようになっています)。メモリ領域は上からテキスト領域、データ領域、bss領域、ヒープ領域、スタック領域となっています。それぞれの領域で保持されるデータは表1.2にまとめました。
領域名 | その領域に格納されるデータ |
テキスト領域 | プログラムコード |
データ領域 | 初期値ありグローバル変数や静的変数 |
bss領域 | 初期値なしグローバル変数や静的変数 |
ヒープ領域 | 動的に確保されるデータ(mallocで確保された領域など) |
スタック領域 | ローカル変数や関数の引数、戻り値(関数内のデータ) |
テキスト領域はプログラムコードが格納されます。データ領域とbss領域はグローバル変数が格納されます。データ領域は初期化されたグローバル変数や静的変数、bss領域は初期化されていないグローバル変数や静的変数が格納されます。ヒープ領域はプログラム内で動的に確保される変数が格納され、スタック領域は関数処理で使用し、関数を抜けるとスタック領域は解放されます。
本書ではスタック領域を頻繁に使用します。具体的なスタック領域の動きは第2章で説明するので、ここではスタックというデータ構造について説明します。スタックはコンピュータで使われるデータ構造のひとつで、LIFO(Last In First Out)方式という、後に入れたデータを先に出す動きをするデータ構造です。スタックにデータを入れるときはpush、スタックの中のデータを取り出す時はpopといいます(pushする、popすると言います)。
先程述べた通り、スタックは後に入れたデータを先に出します(筒のような入れ物にデータを入れるイメージです)。図1.6を見て、そのイメージを覚えてください。