前回までのあらすじ
- 自作PCを作ろう!
- まずメモリを作ったよ!
- ISAを作ったよ!(コンパイラはまだ)
- アセンブリ言語を作ったよ!(コンパイラはまだ)
- CPUを作ったよ!
- 任意のプログラムを実行できるようになったよ!
- キーボード入力を受け付けられるようになったよ!(ただし独力ではない)
- 複数桁+複数桁の足し算が行えるようになったよ!
-
CALL命令とRET命令を実装して,関数呼び出しが可能になったよ!
今回の目標
前回の記事で,関数呼び出しが可能な状態にしました.
しかしこの命令を機械語で記述しようとすると,関数の実態があるプログラムカウンタをハードコーディングしなければならず非常に面倒なことになっていました.
今作成しているPC用のアセンブリ言語をせっかく作っているので,このアセンブリ言語から機械語プログラムにアセンブルするアセンブラを作成することを今回の目標とします.
ここから本題
どんな仕様で作るのか
現状,CPU側ではスタックオーバーフローを検知していません.
なのでアセンブラ側で検知するべきでは…と思ったのですが,再帰関数の存在がある以上,アセンブラ側で関数呼び出しが制限を超えないかどうか完全にチェックすることは不可能です.
ということで,アセンブラでチェックするのはアセンブリ言語の構文エラーとかに限り,関数呼び出しが深くなりすぎる場合の問題はアセンブリ言語でプログラムを書く時の責任とします.
ソース
アセンブラのソースについては以前も載せたような載せなかったような記憶があいまいなのですが,大きく作りが変わっている部分もありそうですし全部載せ直しますね.
今回作成するアセンブラについて簡単に説明すると,自作アセンブリ言語を機械語に翻訳するものです.
自作アセンブリ言語は以下のような形式です.
; セミコロンの後はコメントです
; タブ文字は入力禁止です.コメント内ならOK.インデントは半角スペースで行ってください
; このプログラムで登場する関数を一覧で示します.
; 関数名が被ってはダメです.また,main関数は必須です.
.global func main
; 関数の定義はmain関数から始めます
; 関数名はインデント禁止
main:
mov 0 r0 r0 2h ; 普通に数字を書いたものは普通の数字,先頭に`r`をつけたときはレジスタの番地,末尾に`h`をつけると16進数
call func
ret ; main関数に限らず,全ての関数においてret命令は必須(配置場所は最後ではなくてもエラーにはならない.意味のないソースにはなるが)
; 「.global」で名前を宣言した関数は必ず実装してください
; 同盟の関数を複数回宣言することはできません
func:
; ちなみにインデントのスペースの数に指定はない.なんならなくてもいい
ret
アセンブラの生成物はこんな感じになります.
出力はバイナリファイルではなく,以下のようなsvファイルになります.
※実際にはコメントはありません.また,見やすくするための空行もありません
`include "rom.svh"
`include "machine.svh"
module rom_sv(
rom_read_if.slave rom_read
);
import machine_p::*;
localparam integer ROM_SIZE = 4;
machine_t machines[0:ROM_SIZE - 1] = {
// main関数
machine::mov(0, 0, 0, 33'h1_0000_0000 + 2),
machine::call(0, 33'h1_0000_0000 + 3),
machine::jmp(0, 33'h1_0000_0000 + 2), // すべての処理が終わったら自分に対してジャンプ(無限ループして停止)
// func関数
machine::ret()
};
always_comb begin
if (rom_read.pc >= ROM_SIZE) begin
rom_read.machine = nop();
end else begin
rom_read.machine = machines[rom_read.pc];
end
end
endmodule
補足として,前回作成した命令であるcallとretは,もともとはありませんでしたがアセンブリ言語にはありました.
アセンブリ言語でcallなどが呼ばれた場合は,実際にはjmp命令を利用し,ついでにmovなどでスタックポインタを操作して疑似的に実現していました.
しかし前回の記事で通常の命令としてcallなどを実装したので,アセンブラの実装はかなり簡単になったという経緯があります.
では実際のソースを載せる前に,Claudeによる要約も載せておきます.
僕が説明するよりもきっと正確です.
Claudeによる要約
現在のコードを把握しました。プログラムを読む際の地図として、全体像を説明します。
## このプログラムは何か
**自作CPU(PYNQ-Z2 上で動作)向けのアセンブラ**です。`.asm`(独自アセンブリ言語)を入力し、`machine.svh` の `machine_p` パッケージ関数を使った **SystemVerilog の ROM 初期化コード(`.sv`)** を出力します。`g++ -o asm2bin.exe asm2bin.cpp` でビルドする単一バイナリのCLIツールです。
## ファイル構成(3ファイル)
| ファイル | 役割 |
| ------------- | -------------------------------------------------------------------- |
| `asm2bin.cpp` | 本体。引数処理・`.global`解析・ラベル解決・命令変換・SV出力 |
| `asm2bin.hpp` | 命令テーブル `commands`(命令名→引数仕様)と引数種別 `arg_t` の定義 |
| `util.hpp` | 文字列補助(`str_find_first_of`, `b2d/o2d/h2d` 基数変換, `replace`) |
## 処理の流れ(`asm2bin.cpp` を読む順番)
1. **`main`** → `get_args`(引数解析: `-a` 入力, `-o` 出力。省略時は拡張子を `.sv` に)
2. **`asm2bin`** が `output_header` → `output_bin` → `output_footer` を順に呼ぶ
3. **`output_bin`**(中核, 192行〜):
- `.global` 行を探して `get_function_names` で**全関数名を収集**(アドレスは一旦 `npos`)
- `main` が無ければエラー
- 1行ずつ読み、`func:` ならその時点の `line_num` を関数アドレスとして確定(**main を pc=0 から配置**)。命令行なら `output_bin_line` で機械語化し `line_num++`
- 最後に `function_name2line_num` で**関数名文字列を実アドレス(行番号)に一括置換**して出力
4. **`output_bin_line`**(297行〜): 1命令を `machine::xxx(...)` テキストへ変換
- `convert_arg` が各引数を整形(`rN`→数値、基数変換、即値は `33'h1_0000_0000 + 値`)
## 重要な設計ポイント
**① 関数名の遅延解決**
`call func` の `func` は変換時点では文字列のまま残し、全命令変換後に `function_name2line_num`(489行〜)が行番号へ一括置換します。ここは**長さの降順**で置換します(`f1` が `f11` を壊す接頭辞バグ対策)。
**② call/ret はCPUの正式命令(J系)**
以前はソフト的な疑似命令展開でしたが、現在はハードウェアの `CALL`/`RET` 命令へ1対1変換します。
- `call func` → `machine::call(0, 33'h1_0000_0000 + <行番号>)`(rs1=0+即値ターゲットという特殊形のため `output_call_function` で特別に組立)
- `ret` → `machine::ret()`(引数なし。汎用経路で生成)
- SP初期化・戻り先レジスタ管理はすべてハードウェアが行うため、アセンブラはブートストラップ命令や静的検査を持ちません
## 命令テーブル `commands`(`asm2bin.hpp`)
各命令を `{引数最小数, {引数型...}, imm必須か, imm出力するか}` で定義。`arg_t` は `REGISTER`/`RAW_DATA`(即値)/`FUNC_NAME`(関数名)/`MASK`。命令系統は N(nop)/P(演算)/S(シフト)/A(mov)/F(比較分岐)/J(jmp,call,ret)/M(メモリ)/IO。
## 出力フォーマット
```systemverilog
`include "machine.svh"
module rom( ... pc入力, machine/imm出力 ... );
logic [63:0] machines[0:255] = {
machine::mov(...), // 1命令64bit(上位32=命令本体, 下位32=即値)
machine::call(0, 33'h1_0000_0000 + 3),
...
};
always_comb begin ... machine <= machines[pc][63:32]; ... end
endmodule
```
最大255命令。`pc` を受けて該当行の機械語を返す ROM になります。
---
ちなみに、選択された `test/bin/03.sv` の `machine::ret(),` の次の `machine::mov(...)` は、`func1` 関数の先頭命令(`call func2` から戻った後の処理)にあたります。読み進める際の入口として `output_bin`(192行)と `output_bin_line`(297行)を中心に追うと全体が掴めます。
何か特定の部分を掘り下げますか?
では実際のソースを全部貼ります.
エラーチェックとかを慎重にしているのでかなり長くなっています.
個人で使う分にはこんなに慎重にしなくてもよかったかなって感じはしますが.
util.hpp
#pragma once
#include <math.h>
#include <string>
#include <map>
#include "asm2bin.hpp"
int str_find_first_of(const std::string &str, const char ch); // 最初にchが出現する文字数を返す.なければ末尾までの文字数
std::string ltrim(const std::string &str); // 先頭の半角スペースを除去する
const std::string b2d(const std::string &bin); // 2進数を10進数に変換する
const std::string o2d(const std::string &oct); // 8進数を10進数に変換する
const std::string h2d(const std::string &hex); // 16進数を10進数に変換する
void replace( // 文字列のうち,パターンに当てはまる部分を全て置換する
std::string &source, const std::string &pattern, const std::string &replacement
);
// 最初にchが出現する文字数を返す.なければ末尾までの文字数
int str_find_first_of(const std::string &str, const char ch) {
std::size_t index = str.find_first_of(ch);
if (index == std::string::npos) index = str.length();
return static_cast<int>(index);
}
// 先頭の半角スペースを除去する
std::string ltrim(const std::string &str) {
std::size_t i = 0;
while (i < str.length() && str[i] == ' ') i++;
return str.substr(i);
}
// b2d/o2d/h2d は現在どこからも呼ばれていない(未使用).
// 基数付きの値は convert_arg で Verilog のサイズ付きリテラル(例 6'h2)へ書き直し,
// 実際の数値変換は SystemVerilog 側に任せているため,10進への変換関数は使っていない.
// 将来 C++ 側で実値出力したくなった場合に備えて残してある.
// 2進数を10進数に変換する
const std::string b2d(const std::string &bin) {
const int len = bin.length();
int dec = 0;
for (int i = 0; i < len; i++) {
// 2進数として不正な値
if (bin[i] != '0' && bin[i] != '1') throw "asm syntax error: fail number '" + bin + "'";
// その桁の数字を足す
dec += (bin[i] - '0') * std::pow(2, len - i - 1);
}
return std::to_string(dec);
}
// 8進数を10進数に変換する
const std::string o2d(const std::string &oct) {
const int len = oct.length();
int dec = 0;
for (int i = 0; i < len; i++) {
// 8進数として不正な値
if (oct[i] < '0' || oct[i] > '7') throw "asm syntax error: fail number '" + oct + "'";
// その桁の数字を足す
dec += (oct[i] - '0') * std::pow(8, len - i - 1);
}
return std::to_string(dec);
}
// 16進数を10進数に変換する
const std::string h2d(const std::string &hex) {
const int len = hex.length();
int dec = 0;
for (int i = 0; i < len; i++) {
const char c = hex[i];
// 0から9
if (c >= '0' && c <= '9') {
dec += (c - '0') * std::pow(16, len - i - 1);
}
else if (c >= 'a' && c <= 'f') {
dec += (c - 'a' + 10) * std::pow(16, len - i - 1);
}
else if (c >= 'A' && c <= 'F') {
dec += (c - 'A' + 10) * std::pow(16, len - i - 1);
}
else {
throw "asm syntax error: fail number '" + hex + "'";
}
}
return std::to_string(dec);
}
// 文字列のうち,パターンに当てはまる部分を全て置換する
void replace(
std::string &source, const std::string &pattern, const std::string &replacement
) {
std::size_t patternLength = pattern.length();
int position = 0;
while ((position = source.find(pattern)) != std::string::npos) {
source.replace(position, patternLength, replacement);
}
}
asm2bin.hpp
#pragma once
#include <map>
#include <string>
#include <vector>
// 命令に与えられる引数の種類
enum class arg_t {
REGISTER, // レジスタの番地
RAW_DATA, // イミディエイトデータ (イミディエイトデータの使用フラグも含む)
FUNC_NAME, // 関数名
MASK, // ビットマスク
};
// 各命令の引数情報
typedef struct {
const int arg_num_min; // とりうる引数の最小の個数(イミディエイトデータがある場合は引数の数はこれプラス1になる)
const std::vector<arg_t> arg_types; // それぞれの引数の種類
const bool imm_required; // イミディエイトデータが必須かどうか(必須な場合,引数の個数はarg_num_minから変動しない)
const bool has_imm; // machine.svh関数のimmパラメータを持つか(falseの場合,immは出力しない)
} command_arg_t;
// 機械語の命令一覧
const std::map<std::string, command_arg_t> commands = {
// 処理を実行しない(N系)
{"nop" , {0, { }, false, false}},
// 演算系(P系)
{"and" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER }, false, false}},
{"or" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER }, false, false}},
{"xor" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER }, false, false}},
{"not" , {2, {arg_t::REGISTER, arg_t::REGISTER, }, false, false}},
{"nand" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER }, false, false}},
{"add" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER }, false, false}},
{"sub" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER }, false, false}},
{"mul" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER }, false, false}},
{"div" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
// シフト系(S系)
{"sll" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
{"srl" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
{"sla" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
{"sra" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
// 代入系(A系)
{"mov" , {3, {arg_t::MASK , arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
// 分岐系(F系)
{"eq" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA }, true , true }},
{"ne" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA }, true , true }},
{"lt" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA }, true , true }},
{"gt" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA }, true , true }},
{"elt" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA }, true , true }},
{"egt" , {3, {arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA }, true , true }},
// ジャンプ系(J系)
{"jmp" , {1, {arg_t::REGISTER, arg_t::RAW_DATA }, false, true }},
{"call" , {1, {arg_t::FUNC_NAME }, false, false}}, // 引数は呼び出し先関数名。出力は output_bin_line で特別に組み立てる(rs1=0 + 即値ターゲット)
{"ret" , {0, { }, false, false}}, // 引数なし。汎用経路が machine::ret() を生成する
// メモリ系(M系)
{"rm" , {3, {arg_t::MASK , arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
{"wm" , {3, {arg_t::MASK , arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
{"brm" , {4, {arg_t::MASK , arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
{"bwm" , {4, {arg_t::MASK , arg_t::REGISTER, arg_t::REGISTER, arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
// 標準入出力系(IO系)
{"scan" , {1, { arg_t::REGISTER}, false, false}},
{"print", {1, {arg_t::REGISTER, arg_t::RAW_DATA}, false, true }},
};
// 引数タイプごとのビット数を返す
std::string get_bit_length_of_command(const arg_t arg) {
switch (arg) {
case arg_t::REGISTER:
return std::to_string(6);
case arg_t::RAW_DATA:
return std::to_string(32);
case arg_t::FUNC_NAME:
return std::to_string(6);
case arg_t::MASK:
return std::to_string(4);
default:
// 起きないはずのエラーなのでエラーメッセージは適当
throw std::string("asm syntax error: arg type is fail");
return "";
}
}
asm2bin.cpp
#include <string.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <vector>
#include "asm2bin.hpp"
#include "util.hpp"
// コマンドライン引数情報
typedef struct {
std::string asm_file_name; // アセンブリファイル名
std::string sv_file_name; // 出力ファイル名
} args_t;
// 関数
void get_args(int argc, char **argv, args_t &args); // コマンドライン引数を取得
void asm2bin(std::ifstream &asm_file, std::ofstream &sv_file); // アセンブリをバイナリに変換する
void output_header(std::ofstream &sv_file); // svファイルのヘッダーを出力する
void output_bin(std::ifstream &asm_file, std::ofstream &sv_file); // バイナリ部分を出力する
std::string read_global_line(std::ifstream &asm_file); // .global行まで読み飛ばして返す
void get_function_names( // プログラムに存在する関数の名前を取得する
std::map<std::string, std::size_t> &functions, std::string line
);
void assemble_body( // 本体をアセンブルしfunctionsとinstructionsを埋める
std::ifstream &asm_file, std::map<std::string, std::size_t> &functions,
std::vector<std::string> &instructions
);
void output_bin_line( // アセンブリ一行を機械語化しinstructionsへ追加
std::vector<std::string> &instructions,
const std::map<std::string, std::size_t> &functions, std::string line
);
std::string convert_arg( // 機械語関数の引数を加工して返す
const std::map<std::string, std::size_t> &functions,
const std::string &arg, const command_arg_t &command_arg, const int arg_num
);
void validate_arg_count( // 引数の個数が命令の仕様に合うか検証する
const command_arg_t &command_arg, const int arg_num, const std::string &command
);
std::string get_machine_function_name(const std::string &command); // machine.svh側の関数名へ変換する
void throw_if_tab(const std::string &line); // タブ文字があればエラーにする
void apply_main_self_loop( // mainが到達する最初のretを自己ループに置換する
std::vector<std::string> &instructions
);
std::string join_instructions( // 命令を結合する(末尾カンマ無し)
const std::vector<std::string> &instructions
);
std::string function_name2line_num( // 関数参照を行番号に置換する
const std::map<std::string, std::size_t> &functions, const std::string &bin
);
void output_footer(std::ofstream &sv_file); // svファイルのフッターを出力する
const int MAX_LINE_NUM = 255; // 出力されるアセンブリプログラムの最大行数
const char FUNC_REF_DELIM = '@'; // 出力本体で関数参照を囲む区切り文字(命令名や数値との衝突を防ぐ)
// メイン関数
// 処理に成功したら0,失敗したら1を返り値にする
int main(int argc, char **argv) {
args_t args; // コマンドライン引数
std::ifstream asm_file; // アセンブリファイル
std::ofstream sv_file; // 出力ファイル
// コマンドライン引数を取得
get_args(argc, argv, args);
// コマンドライン引数の取得に失敗していれば
if (
// アセンブリファイル名
args.asm_file_name.empty()
// 出力ファイル名
|| args.sv_file_name.empty()
) {
std::cout << "fail args" << std::endl;
return 1;
}
// アセンブリファイルを開く
asm_file.open(args.asm_file_name);
if (!asm_file) {
std::cout << "cannot open asm file: " << args.asm_file_name << std::endl;
return 1;
}
// 出力ファイルを開く
sv_file.open(args.sv_file_name);
if (!sv_file) {
std::cout << "cannot open sv file: " << args.sv_file_name << std::endl;
return 1;
}
// アセンブリ言語をバイナリに変換する
try {
asm2bin(asm_file, sv_file);
// 正常終了を報告
std::cout << "assembled: " << args.sv_file_name << std::endl;
// ファイルを閉じる
asm_file.close();
sv_file.close();
return 0;
}
catch (std::string msg) {
std::cout << msg << std::endl;
return 1;
}
}
// コマンドライン引数を取得
// -a: 必須引数.アセンブリファイル名.
// -o: 出力ファイル名.省略した場合,アセンブリファイル名の拡張子を変更して同階層に出力される.
// 何も指定せずに引数を置いた場合,アセンブリファイル名と解釈される.
void get_args(int argc, char **argv, args_t &args) {
// 全ての引数でループ(コマンド名は飛ばす)
for (int i = 1; i < argc; i++) {
const char *arg = argv[i]; // 引数一つ
// 指定子なら
if (arg[0] == '-') {
std::string kind = arg; // 指定を保存
// インクリメントして次のパラメータを取得
i++;
if (i >= argc) break;
arg = argv[i];
// 指定されたパラメータを保存
if (kind == "-a") args.asm_file_name = argv[i];
else if (kind == "-o") args.sv_file_name = argv[i];
}
// 指定子の直後ではないなら
else {
args.asm_file_name = argv[i];
}
}
// アセンブリファイル名が .asm で終わっているか(短い名前での範囲外アクセスを防ぐ)
const bool asm_name_ok =
args.asm_file_name.length() >= 4
&& args.asm_file_name.substr(args.asm_file_name.length() - 4) == ".asm";
// 出力ファイル名が指定されていないなら,アセンブリ名の拡張子を .sv にして使う
if (asm_name_ok && args.sv_file_name.empty()) {
// いったんアセンブリファイル名を入れる
args.sv_file_name = args.asm_file_name;
// 拡張子を更新
args.sv_file_name.replace(
args.sv_file_name.length() - 3, // 置換するのは後ろから三文字
3, // 置換する文字数
"sv" // 拡張子は「.sv」にする
);
}
// 出力ファイル名が .sv で終わっているか
const bool sv_name_ok =
args.sv_file_name.length() >= 3
&& args.sv_file_name.substr(args.sv_file_name.length() - 3) == ".sv";
// コマンドライン引数が不正ではないことをチェック
if (!asm_name_ok || !sv_name_ok) {
// メッセージ出力
std::cout << "args fail" << std::endl
<< "-a: asm file name. e.g. ~~.asm" << std::endl
<< " actual: " << args.asm_file_name << std::endl
<< "-o: output file name. e.g. ~~.sv" << std::endl
<< " actual: " << args.sv_file_name << std::endl;
// 後の処理でエラーになるよう,コマンドライン引数をクリア
args.asm_file_name.clear();
args.sv_file_name.clear();
}
}
// アセンブリをバイナリに変換する
void asm2bin(std::ifstream &asm_file, std::ofstream &sv_file) {
// ヘッダーを出力する
output_header(sv_file);
// バイナリ部分を出力する
output_bin(asm_file, sv_file);
// フッターを出力する
output_footer(sv_file);
// バッファに溜まっている分を出力
sv_file.flush();
}
// svファイルのヘッダーを出力する
void output_header(std::ofstream &sv_file) {
sv_file << "`include \"rom.svh\"\n"
<< "`include \"machine.svh\"\n"
<< "\n"
<< "module rom_sv(\n"
<< " rom_read_if.slave rom_read\n"
<< " );\n"
<< " import machine_p::*;\n"
<< "\n";
}
// バイナリ部分を出力する
void output_bin(std::ifstream &asm_file, std::ofstream &sv_file) {
std::map<std::string, std::size_t> functions; // 関数とその開始pc
std::vector<std::string> instructions; // 機械語にした命令一覧(1要素=1命令)
// .global 行を取得し,宣言された関数名を読み込む
std::string global_line = read_global_line(asm_file);
get_function_names(functions, global_line);
// main関数が指定されていなければ
if (functions.find("main") == functions.end()) {
throw std::string("asm syntax error: main function not found");
}
// 本体をアセンブルする(functions のpc確定 + instructions 生成)
assemble_body(asm_file, functions, instructions);
// .global で宣言された関数がすべて定義されているか確認する
// pcがnposのまま残っていれば,宣言だけで定義(ラベル)がない関数
for (const auto &function : functions) {
if (function.second == std::string::npos) {
throw "asm syntax error: declared but not defined function '" + function.first + "'";
}
}
// mainが到達する最初のretを自己ループに置き換える
apply_main_self_loop(instructions);
// 命令数をlocalparam,machine_t配列として出力する
const std::string body = function_name2line_num(functions, join_instructions(instructions));
sv_file << " localparam integer ROM_SIZE = " << instructions.size() << ";\n\n";
sv_file << " machine_t machines[0:ROM_SIZE - 1] = {\n";
sv_file << body;
sv_file << " };\n";
}
// .global行まで読み飛ばして返す
// .global より前は空行とコメント行(;)のみ許可し,それ以外はエラーにする
std::string read_global_line(std::ifstream &asm_file) {
std::string line;
while (getline(asm_file, line)) {
// .global 行が見つかったら(タブ非対応を確認して)返す
if (strncmp(".global ", line.c_str(), strlen(".global ")) == 0) {
throw_if_tab(line);
return line;
}
// 空行でもコメント行でもなければ,.global より前のコードとしてエラー
std::string trimmed = ltrim(line);
if (!trimmed.empty() && trimmed[0] != ';') {
throw "asm syntax error: code before .global '" + line + "'";
}
}
// .global 宣言が無いままEOFに達した
throw std::string("asm syntax error: .global not found");
}
// プログラムに存在する関数の名前を取得する
void get_function_names(
std::map<std::string, std::size_t> &functions, std::string line
) {
// 関数指定が正しくなければ
if (strncmp(".global ", line.c_str(), strlen(".global ")) != 0) {
throw std::string("asm syntax error: .global fail");
}
// 関数の羅列部分を取得
line = line.substr(strlen(".global "));
// 関数名一覧を取得
for (int i = 0; i < static_cast<int>(line.length()); i++) {
// スペースならスキップ
if (line[i] == ' ') continue;
// スペース以外なら,カンマまでを関数名として記録
std::string word = line.substr(i); // 厳密には一単語ではないが便宜上wordと呼ぶ
int last_index = str_find_first_of(word, ',');
std::string function_name = word.substr(0, last_index);
// 末尾の空白を除去(カンマの前に空白がある場合に備える)
while (!function_name.empty() && function_name.back() == ' ') {
function_name.pop_back();
}
// すでにその名前の関数が登録されていれば
if (functions.find(function_name) != functions.end()) {
throw "asm syntax error: function name fail '" + function_name + "'";
}
functions[function_name] = std::string::npos; // いったんnposを入れる
// 関数名の長さぶんiに加算
i += last_index;
}
}
// 本体をアセンブルしfunctionsとinstructionsを埋める
// 命令のpcは instructions のインデックスに対応する
void assemble_body(
std::ifstream &asm_file, std::map<std::string, std::size_t> &functions,
std::vector<std::string> &instructions
) {
std::string line; // アセンブリファイルの一文
std::string current_function; // 現在変換中の関数名
bool current_has_ret = false; // 現在の関数が ret を含むか
while (getline(asm_file, line)) {
// 空行はスキップ
if (line == "") continue;
// 空白のみの行・コメント行はスキップ(コメント内のコロンをラベルと誤認しないため)
std::string trimmed = ltrim(line);
if (trimmed.empty() || trimmed[0] == ';') continue;
// 関数名の宣言なら
std::size_t colon_index = line.find_first_of(':');
if (colon_index != std::string::npos) {
std::string function_name = line.substr(0, colon_index);
// 関数一覧にないなら
if (functions.find(function_name) == functions.end()) {
throw "asm syntax error: not defined function '" + function_name + "'";
}
// すでにセット済みなら
if (functions[function_name] != std::string::npos) {
throw "asm syntax error: function overlapping definition '" + function_name + "'";
}
// 最初に宣言された関数がmainではない
if (instructions.empty() && function_name != "main") {
throw "asm syntax error: first function is not main '" + function_name + "'";
}
// 直前の関数が ret を1つも持たないならエラー
if (!current_function.empty() && !current_has_ret) {
throw "asm syntax error: function without ret '" + current_function + "'";
}
// 関数の先頭pcを記録し,現在の関数を更新する
functions[function_name] = instructions.size();
current_function = function_name;
current_has_ret = false;
continue;
}
// main関数の宣言前にコードがある
if (functions["main"] == std::string::npos) {
throw std::string("asm syntax error: not found main function");
}
// 現在の関数が ret を含むか記録する
std::string command = trimmed.substr(0, str_find_first_of(trimmed, ' '));
if (command == "ret") current_has_ret = true;
// アセンブリを機械語にしてinstructionsに追加する
output_bin_line(instructions, functions, line);
// 最大行数を超えた
if (static_cast<int>(instructions.size()) >= MAX_LINE_NUM) {
throw std::string("asm syntax error: line more than 255");
}
}
// 最後の関数が ret を1つも持たないならエラー
if (!current_function.empty() && !current_has_ret) {
throw "asm syntax error: function without ret '" + current_function + "'";
}
}
// アセンブリ一行を機械語化しinstructionsへ追加する
void output_bin_line(
std::vector<std::string> &instructions,
const std::map<std::string, std::size_t> &functions, std::string line
) {
// タブ文字は非対応
throw_if_tab(line);
// 先頭のスペースを除去する
line = ltrim(line);
// セミコロンなら
if (line[0] == ';') return;
// 命令を取得
std::string command = line.substr(0, str_find_first_of(line, ' '));
// 命令が不正なら
if (commands.find(command) == commands.end()) {
throw "asm syntax error: fail command '" + command + "'";
}
// 命令がcallなら
// callは「rs1=0 + 即値ターゲット」という特殊な出力形のため,汎用経路に乗らない
// 戻り先アドレスの保存やSPの更新はCPU側が行うので,呼び出し先pcを即値で渡すだけでよい
if (command == "call") {
// 引数前のスペースを除去して呼び出し先関数名を取得
line = ltrim(line.substr(std::min(command.length() + 1, line.length())));
const int first_space = str_find_first_of(line, ' ');
const std::string target = convert_arg(
functions, line.substr(0, first_space), commands.at("call"), 0
);
// callの引数は1つだけ.呼び出し先の後に(コメント以外の)余分な引数があればエラー
const std::string rest = ltrim(line.substr(first_space));
if (!rest.empty() && rest[0] != ';') {
throw "asm syntax error: too many arguments '" + command + "'";
}
instructions.push_back("call(0, 33'h1_0000_0000 + " + target + ")");
return;
}
// 命令の引数仕様
const command_arg_t &command_arg = commands.at(command);
// 命令本体を組み立てる
std::string instr = get_machine_function_name(command) + "(";
// 引数を取得
int arg_num = 0;
line = line.substr(std::min(command.length() + 1, line.length())); // 引数がなかった時のためstd::min
while (!line.empty() && line[0] != ';') {
// スペースを飛ばす
if (line[0] == ' ') {
line = line.substr(1);
continue;
}
// 引数が引数仕様の個数より多い(arg_typesの範囲外アクセスを防ぐ)
if (arg_num >= static_cast<int>(command_arg.arg_types.size())) {
throw "asm syntax error: too many arguments '" + command + "'";
}
// 引数を追加
if (arg_num != 0) instr += ", ";
int first_space = str_find_first_of(line, ' ');
instr += convert_arg(
functions, line.substr(0, first_space), command_arg, arg_num
);
// 次のループの準備
line = line.substr(first_space); // 引数直後のスペースは飛ばさない.最後の引数である可能性があるため
arg_num++;
}
// 引数の数があっているか確認
validate_arg_count(command_arg, arg_num, command);
// immあり・未出力なら,0にしておく
if (command_arg.has_imm && !command_arg.imm_required && (arg_num == command_arg.arg_num_min)) {
instr += ", 0";
}
// 命令を閉じて追加する
instr += ")";
instructions.push_back(instr);
}
// ニーモニックをmachine.svh側の関数名に変換する
// SystemVerilog予約語と衝突するand/or/xor/not/nandは末尾に_を付ける
std::string get_machine_function_name(const std::string &command) {
if (command == "and") return "and_";
if (command == "or") return "or_";
if (command == "xor") return "xor_";
if (command == "not") return "not_";
if (command == "nand") return "nand_";
return command;
}
// 機械語関数の引数を加工して返す
std::string convert_arg(
const std::map<std::string, std::size_t> &functions,
const std::string &arg, const command_arg_t &command_arg, const int arg_num
) {
std::string converted_arg = arg; // 引数は加工できないので,加工用の変数を用意
// 引数が関数名なら (命令がcallの場合は関数名が引数になる)
if (functions.find(converted_arg) != functions.end()) {
// 関数名を区切り文字で囲んで返す
// function_name2line_num が囲まれたトークンだけを行番号へ置換するため,
// 関数名が命令名や数値の一部と一致して誤置換されることを防げる
return FUNC_REF_DELIM + converted_arg + FUNC_REF_DELIM;
}
// 引数がレジスタなら
if (converted_arg[0] == 'r') {
// 引数タイプが違うなら
if (command_arg.arg_types[arg_num] != arg_t::REGISTER) {
throw "asm syntax error: arg register address fail '" + arg + "'";
}
converted_arg = arg.substr(1);
}
else {
// 引数タイプがマスクまたは生の値ではないなら
if (command_arg.arg_types[arg_num] != arg_t::MASK && command_arg.arg_types[arg_num] != arg_t::RAW_DATA) {
throw "asm syntax error: arg mask or raw data fail '" + arg + "'";
}
}
// 加工後に空文字列なら不正な引数(例: 番号のない "r")
if (converted_arg.empty()) {
throw "asm syntax error: fail arg '" + arg + "'";
}
// 引数が十進数表記ではないなら
const char last = converted_arg[converted_arg.length() - 1];
if (last < '0' || last > '9') {
switch (last) {
case 'b': case 'o': case 'h': // 2進数,8進数,16進数
// Verilogでの表記に書き直す
converted_arg = get_bit_length_of_command(command_arg.arg_types[arg_num])
+ '\'' + last
+ converted_arg.substr(0, converted_arg.length() - 1);
break;
default:
throw std::string("asm syntax error: fail base number '") + last + "'";
}
}
// イミディエイトデータを使用するなら
if (command_arg.arg_types[arg_num] == arg_t::RAW_DATA) {
converted_arg = "33'h1_0000_0000 + " + converted_arg;
}
// 加工した引数を返す
return converted_arg;
}
// 引数の個数が命令の仕様に合うか検証する
void validate_arg_count(
const command_arg_t &command_arg, const int arg_num, const std::string &command
) {
if (
// イミディエイトデータが必須で,引数の個数が違う
(command_arg.imm_required && arg_num != command_arg.arg_num_min)
// immあり・省略可で,引数の個数が違う(arg_num_min または arg_num_min+1 が有効)
|| (!command_arg.imm_required && command_arg.has_imm && arg_num != command_arg.arg_num_min && arg_num != command_arg.arg_num_min + 1)
// immなしで,引数の個数が違う(arg_num_min のみ有効)
|| (!command_arg.imm_required && !command_arg.has_imm && arg_num != command_arg.arg_num_min)
) {
throw "asm syntax error: fail program " + command + " " + std::to_string(arg_num);
}
}
// タブ文字があればエラーにする
void throw_if_tab(const std::string &line) {
if (line.find('\t') != std::string::npos) {
throw "asm syntax error: tab character is not supported '" + line + "'";
}
}
// mainが到達する最初のretを自己ループに置換する
// プログラムはmain(pc=0)から実行されるため,先頭から線形に見て最初に現れるretが
// mainがCALLされずに到達するret=戻り先の無いretになる.これを自分自身へのjmp(無限ループ)に
// 置き換えてmainを停止させる(mainは必ずretを持つので必ず見つかる)
void apply_main_self_loop(std::vector<std::string> &instructions) {
for (std::size_t pc = 0; pc < instructions.size(); pc++) {
if (instructions[pc] == "ret()") {
instructions[pc] =
"jmp(0, 33'h1_0000_0000 + " + std::to_string(pc) + ")";
return; // 最初の1つだけ置換する
}
}
}
// 命令を結合する(各命令を8スペースインデントし,カンマ区切りで並べる)
// SystemVerilogの配列初期化子では末尾カンマが構文エラーになるため,末尾要素にはカンマを付けない
std::string join_instructions(const std::vector<std::string> &instructions) {
std::string body;
for (std::size_t i = 0; i < instructions.size(); i++) {
body += " " + instructions[i];
if (i + 1 < instructions.size()) body += ",";
body += "\n";
}
return body;
}
// 関数参照を行番号に置換する
std::string function_name2line_num(
const std::map<std::string, std::size_t> &functions, const std::string &bin
) {
std::string rtn = bin; // 引数は加工できないので,加工用の変数を用意
// 区切り文字で囲まれた関数参照(@func@ など)を対応する行番号に置換する
// 区切り文字で囲んでいるため,f1 と f11 のような接頭辞の衝突や,
// 関数名が命令名・数値の一部に一致することによる誤置換が起きない
for (const auto &function : functions) {
replace(
rtn,
FUNC_REF_DELIM + function.first + FUNC_REF_DELIM,
std::to_string(function.second)
);
}
return rtn;
}
// svファイルのフッターを出力する
void output_footer(std::ofstream &sv_file) {
sv_file << "\n"
<< " always_comb begin\n"
<< " if (rom_read.pc >= ROM_SIZE) begin\n"
<< " rom_read.machine = nop();\n"
<< " end else begin\n"
<< " rom_read.machine = machines[rom_read.pc];\n"
<< " end\n"
<< " end\n"
<< "\n"
<< "endmodule\n";
}
今後の展望
これでアセンブリ言語から機械語プログラムを生成できるようになりました.
しかしちょっと面倒なことがあります.
正直,アセンブリ言語のプログラムを書くのも面倒というか大変ですよね.
という事で次回は,C言語プログラムをアセンブリ言語に翻訳するコンパイラを作成しようと思います.
だいぶ大変そうですが最近のAIは優秀なので何とかなるでしょう.