本投稿、[新卒が作る自作OS]は、我々が自作OSを作るにあたり、詰まったところや、備忘録的に残しておきたいところなどをまとめておこうという趣旨の投稿です。
今回は、OSを開発する上でなくてはならないアセンブリ言語の扱いについてまとめました。高級言語の例としてC言語からアセンブリ言語を呼び出す方法を紹介します。今回の実行環境は以下の表のようになります。
環境 | バージョン |
プロセッサ | x64 |
OS | Windows 10 Pro |
コンパイラ | gcc version 9.2.0 |
高級言語からアセンブリ言語を呼びたいとき
プログラミング言語において、アセンブリ言語のような機械語と一対一で対応する言語を低級言語(低水準言語)、C言語のように人間に対する可読性を高めた言語を高級言語(高水準言語)と呼びます。
OSを作るにあたって、低級言語と高級言語のどちらを使うべきでしょうか。OSはかなりの規模のプログラムを書くことが予想されるので、可能であれば人間にとってわかりやすい高級言語を使いたいところです。しかし、高級言語をOS開発に使用するには2つの問題点があります。
まず一つ目は高級言語の命令はOS依存であるという点です。高級言語は、人間にとって理解しやすくするため、様々なリソースに対する命令をまとめて抽象化しています。そして[新卒が作る自作OS] OSって何?でも説明されていますが、ハードウェア資源の抽象化を行うのがOSであり、アプリケーションはAPI(Application Programming Interface)を通してハードウェア資源にアクセスできます。すなわち、高級言語の持つ機能の多くはOS依存であるためOS開発には使えないのです。
もう一つは高級言語ではできないことがあるという点です。高級言語は基本的にアプリケーションを開発することを前提として設計されています。そのため、アプリケーション開発に不要とされる機能は省かれています。例えば、OS開発に必要な信号入出力を行うIN命令やOUT命令、割り込み処理を管理するCLI(CLear Interrupt flag)命令やSTI(SeT Interrupt flag)命令などは、ほとんどの高級言語で機能としてありません。
これら2つの理由から高級言語だけではOS開発ができません。そのため、部分的に低級言語であるアセンブリ言語を扱う必要があるのです。
C言語からアセンブリ言語を呼び出す方法
C言語からアセンブリ言語を呼び出すにはインラインアセンブリとアセンブリファイルから呼び出す方法の二種類があります。
インラインアセンブリ
インラインアセンブリは高級言語のソースファイル内でアセンブリ言語を呼び出す方法です。手軽に呼び出せる反面、アセンブリコードが長いと複雑になるので、処理内容が短い場合に適した方法です。
インラインアセンブリを書くには以下のように記述します。
__asm__("【アセンブリ言語命令】" :【出力用オペランド】 :【入力用オペランド】 :"【上書きレジスタ】" );
出力用オペランド、入力用オペランドはC言語の変数の受け渡しを行います。出力用オペランドにはアセンブリ言語からC言語が受け取る変数を、入力用オペランドにはC言語からアセンブリ言語に渡す変数を指定します。以下のように記述します。
[【マクロ名】] "【オペランド制約】" (【変数名】)
マクロ名はアセンブリ言語側で使う変数名です。指定しなければ記述した順番に「%0」、「%1」と割当られていきます。オペランド制約ではレジスタなのか即値なのか、書き込み専用かなどの制約を指定します。オペランド制約とオプションとして使用できる制約修飾子は以下の表の通りです。変数名はC言語側の変数名です。いずれも不要な場合には省略できます。
オペランド制約 | 意味 |
r | 汎用レジスタ |
I | 即値(定数) |
M | 2のべき乗(2^【変数名】) |
m | メモリアドレス |
制約修飾子 | 意味 |
= | 書き込み専用オペランド |
+ | 読み書き可能オペランド |
& | 出力専用レジスタ(=、+と併用可能) |
上書きレジスタはアセンブリ言語の命令の実行時に使用するレジスタを指定します。指定していないレジスタの値を上書きすると誤作動を起こす可能性があります。
以下がインラインアセンブリのサンプルコードです。C言語ではできないCLI命令と、それだけでは動いているかわかりにくいのでINC命令を表示しています。
#include <stdio.h> int main(void) { int a = 1; __asm__("inc %0\n\t" :"+r" (a)); printf("a++ = %d\n", a); __asm__("cli"); return 0; }
インラインアセンブリを記述するには一つ注意点があります。x86のアセンブリ言語には、Intel 記法とAT&T記法という2種類の記法があります。一般的にはIntel 記法の方が広く用いられています。しかし今回使用しているC言語のコンパイラであるGCCはAT&T記法を使ったインラインアセンブリにのみ対応しています。そのため、今回の環境でインラインアセンブリを実装するにはAT&T記法を使う必要があります。Intel 記法とAT&T記法には様々な違いがありますが、最も大きな違いはオペランドの順番が違う場合が多いことです。例えばADD命令はIntel 記法の場合第一オペランドに第二オペランドの値を加えますが、AT&T記法では第二オペランドに第一オペランドの値を加えます。そのため、インラインアセンブリでADD命令を使う場合以下のようになります。
__asm__("add %1, %0": "+&r" (b):"r"(a));
アセンブリファイル
アセンブリ言語の命令を記述したファイルを作成し、リンカで結合することで呼び出す方法です。一般的なC言語のファイル分割に近い方法になります。ファイルを別に作成する分呼び出し処理を書く手間がかかりますが、可読性は高まるので処理内容が長い場合に適した方法です。
アセンブリファイルをC言語から呼び出すには、アセンブリ言語側に.globl宣言を、C言語側にプロトタイプ宣言を記述します。
以下がアセンブリファイルを使ったサンプルコードです。インラインと同様にCLI命令とINC命令を実装しました。上がC言語側の「asm_main.c」、下がアセンブリ言語側の「asm_func.s」です。
#include <stdio.h> void cli(void); int inc(int); int main(void) { int a = 1; a = inc(a); printf("a++ = %d\n", a); cli(); return 0; }
.globl _cli .globl _inc .text _cli: cli ret _inc: incl %eax ret
まとめ
この記事ではC言語からアセンブリ言語を呼び出す方法をまとめました。
- 高級言語はアプリケーションを開発することを想定しているのでOS開発に必要な一部の機能が使えない。
- OS開発をするにはアセンブリ言語を呼び出す必要がある
- C言語からアセンブリ言語を呼び出すにはインラインアセンブリとアセンブリ言語ファイルの2種類の方法がある
最後までお読みいただきありがとうございます。