動機
原始的なアセンブラしか無い原始的な CPU のプログラミングをすることになった.
でアセンブリプログラミングの一番の萎えポイントとして,例えば C 言語だと
if(a == b){
c = d;
}else{
e = f;
}
みたいに書けるところが,構造化記述できないアセンブラだと
cmp a, b
jnz label0
mov c, d
jmp label1
label0:
mov e, f
label1:
とラベルやら分岐命令やら,自分で生成しないといけないのでめんどくさいし,if がネストすると可読性も悪い.真面目にやるなら bison/flex とかでまともな構造化アセンブリ言語を設計するところだけど,そこまでやるのはなぁ... と思ったところで,C++ のクラス / 演算子オーバーロードをうまいこと使えば,構造化アセンブリプログラミングもどきが出来るのでは? と思った.
例えば,C++ で "r0 = r1;" と書いてコンパイル・実行すれば,"mov r0, r1" というテキストが得られる,みたいなイメージ.
まずは代入演算
//////////////////////////////////////////////////////////////////////////////
// Register
class RegisterObject {
public:
RegisterObject(const char* szName) : m_szName(szName){}
const char *Name(void) const {return m_szName;}
private:
const char* m_szName;
};
class GpReg : public RegisterObject {
public:
GpReg(const char* szName) : RegisterObject(szName){}
GpReg& operator=(const GpReg& src){
printf("\tmov\t%s, %s\n", Name(), src.Name());
return *this;
}
};
//////////////////////////////////////////////////////////////////////////////
// Register インスタンス
GpReg r0("r0");
GpReg r1("r1");
GpReg r2("r2");
GpReg r3("r3");
//////////////////////////////////////////////////////////////////////////////
// アセンブリプログラム
int main(int argc, char **argv){
r0 = r1 = r2;
return 0;
}
GpReg class は汎用レジスタをイメージしていて,特殊なレジスタがあれば RegisterObject か GpReg を継承する感じ.C++ ソースコード上の変数名 (r0 とか) は実行時には失われてしまうので,m_szName に変数名をセットしておく.
代入演算のキモは言うまでもなく "operator=" で,= が呼ばれたら mov 命令のテキストを出力する.
で実行結果:
mov r1, r2
mov r0, r1
おお,いい感じ.アセンブラだと r0 = r2 が直接代入できないので一旦 r1 を経由する,みたいなケースが多々あるが,それが 1行で書けるのはありがたい.
比較演算
次に,if-else-endif の構造化をやる前に比較演算子を定義する.
対象 CPU は,== なら cmpeq みたいに比較演算子毎に比較命令があり,その結果をフラグレジスタ f0 にセットする.条件分岐命令は f0 の値をみて分岐するかどうか決める.
class FlagReg : public RegisterObject {
public:
FlagReg(const char* szName) : RegisterObject(szName){}
};
//////////////////////////////////////////////////////////////////////////////
// Register インスタンス
FlagReg f0("f0");
//////////////////////////////////////////////////////////////////////////////
// global な operator
FlagReg& operator==(const GpReg& a, const GpReg& b){
printf("\tcmpeq\t%s, %s, f0\n", a.Name(), b.Name());
return f0;
}
//////////////////////////////////////////////////////////////////////////////
// アセンブリプログラム
int main(int argc, char **argv){
r0 == r1;
return 0;
}
RegisterObject を継承して FlagReg を定義する.
operater== で,== が呼ばれたら cmpeq 命令を出力して,== の返り値として f0 を返す.
で実行結果:
cmpeq r0, r1, f0
これはなんの問題もない.
if-else-endif
そしてこの取り組みの一番の目的である,if-else-endif の構造化をやってみる.
//////////////////////////////////////////////////////////////////////////////
// 構造化構文
int g_LabelCnt = 0;
std::vector<int> g_Label;
void _if(FlagReg& f){
printf("\tjnset\t%s, _L%d\n", f.Name(), g_LabelCnt);
g_Label.push_back(g_LabelCnt);
++g_LabelCnt;
}
void _else(void){
printf("\tjmp\t_L%d\n", g_LabelCnt);
printf("_L%d:\n", g_Label[g_Label.size() - 1]);
g_Label.pop_back();
g_Label.push_back(g_LabelCnt);
++g_LabelCnt;
}
void _endif(void){
printf("_L%d:\n", g_Label[g_Label.size() - 1]);
g_Label.pop_back();
}
//////////////////////////////////////////////////////////////////////////////
// アセンブリプログラム
int main(int argc, char **argv){
_if(r0 == r1);
r0 = r2;
_else();
r1 = r3;
_endif();
return 0;
}
_if ではフラグレジスタを受取り,必要な分岐命令を生成する.また if-else-endif はネストするので,分岐先ラベルの情報はスタックに push / pop する必要がある.
で実行結果:
cmpeq r0, r1, f0
jnset f0, _L0
mov r0, r2
jmp _L1
_L0:
mov r1, r3
_L1:
おおぉ,これこれ! これがやりたかったんだよ.この時点でこのやり方はかなりうまくいく感触を得ていたが,念の為 if がネストするケースをテストしてみたら,
コード:
//////////////////////////////////////////////////////////////////////////////
// アセンブリプログラム
int main(int argc, char **argv){
_if(r0 == r1);
_if(r1 == r2);
r2 = r0;
_else();
r3 = r1;
_endif();
_else();
r1 = r3;
_endif();
return 0;
}
実行結果:
cmpeq r0, r1, f0
jnset f0, _L0
cmpeq r1, r2, f0
jnset f0, _L1
mov r2, r0
jmp _L2 ←※ここ
_L1:
mov r3, r1
_L2:
jmp _L3
_L0:
mov r1, r3
_L3:
んー,間違いではないんだけど,jmp _L2 の飛び先は jmp _L3 しか無いので,最適化の観点では「ここ」で jmp _L3 にすべき.
この最適化をやるためには,直接アセンブリテキストを出力するのではなく,一旦中間言語とかでメモリ上に溜めておき,最後に最適化フェーズを流す,等しないといけないということがわかった.
それを解決して,あとはメモリアクセスとかラベルへのサブルーチンコールとかを実装すれば,普通に使えそう.