* kcas #1
-(by [[K]], 2025.04.30)
** (0) kcasとはなんですか?
-Kが考えた新しいアセンブラの構文です。構想はだいぶ前から持っていたのですが、きちんとした内容にまとめたのは2025年からです。
** (1) 基本構文-1
-ここでは話を分かりやすくするために、x86向けのアセンブラに限定して書きます。
void casMain()
{
casMain_init;
ORG(0x100);
MOV8RI(AH, 0x02); // AH=02: 1文字出力.
MOV8RI(DL, 0x20);
LB(lp);
INC16R(DX);
CMP8RI(DL, 0x7f);
JNE(lp);
}
-ORG命令は説明不要として、次のMOV8RIを説明すると、これは MOV 命令の8bit版の「レジスタ,即値」形式です。
-同様に、INC16RはINC命令の16bit版の「レジスタ」形式ですし、CMP8RIはCMP命令の8bit版の「レジスタ,即値」形式になります。
-JNEはJNE命令になります。LB命令はラベル宣言命令です。
-ちなみにこのプログラムはINT21Hをあえて書いてないので何も出力しません。
-このアセンブラは、C言語の文法を利用して構成されています。セミコロンで切ってあるので、1行にいくつでも命令を並べることができますし、コメントも入れられます。#defineも使えますし、enumも使えます。
-C言語のルールなので、大文字小文字は区別されます。kcasでは[[K]]の趣味により、命令もレジスタ名もすべて大文字で書きます。
** (2) メリット-1
-このkcasにはどんなメリットがあるでしょうか。それは、以下のようなファイルをインクルードしてコンパイルして実行すれば、NASM形式でのプログラムに簡単に変換できることです。
// kcas to nasm
#include <stdio.h>
#define casMain_init ;
void casMain();
int main() { casMain(); return 0; }
#define MOV8RI(r, i) MOV8RI_(#r, i)
void MOV8RI_(const char *r, int i) { printf(" MOV %s,%d\n", r, i); }
#define INC16R(r) INC16R_(#r)
void INC16R_(const char *r) { printf(" INC %s\n", r); }
// 以下略
** (3) メリット-2
-もう一つのメリットとして、以下のようなファイルをインクルードしてコンパイルすれば、標準的なC言語が使える環境なら、どこにでも移植できるということです(実行環境はx86に限定されません)。
// kcas to C
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
void casMain();
int main() { casMain(); return 0; }
uint16_t ax, cx, dx, bx, sp, bp, si, di;
char CF, ZF, SF, OF;
#define casMain_init int32_t i32
#define MOV8RI(r, i) MOV8RI_ ## r(i)
#define MOV16RI(r, i) MOV16RI_ ## r(i)
#define INC16R(r) INC16R_ ## r()
#define CMP8RI(r, i) CMP8RI_ ## r(i)
#define LB(l) l:
#define al (ax & 0xff)
#define dl (dx & 0xff)
#define ah (ax >> 8)
#define dh (dx >> 8)
#define MOV8RI_AL(i) ax = (ax & 0xff00) | ((i) & 0xff)
#define MOV8RI_AH(i) ax = (ax & 0x00ff) | ((i) & 0xff) << 8
#define MOV8RI_DL(i) dx = (dx & 0xff00) | ((i) & 0xff)
#define MOV16RI_AX(i) ax = (i)
#define JNE(lb) if (ZF == 0) goto lb
#define INC16R_DX() dx++; ZF = (dx == 0); SF = dx >> 15; OF = ((dx & 0x7fff) == 0)
#define CMP8RI_DL(i) i32 = dl - (i); CF = (i32 < 0); ZF = (i32 == 0); SF = (i32 >> 15) & 1; /* todo OF */
#define ORG(i) /* ORG */
-ということで、kcasで一度書いておけば、エミュレータを使わずに高速に実行できますし(アセンブラなのに移植性が高い?笑)、NASMでもMASMでもGNU-asにも変換できるわけです(原理的には)。
** (4) 基本構文-2
-アセンブラにはCALL/RET命令があります。これを実現するために、書き方を拡張します。
void casMain()
{
casMain_init;
ORG(0x100);
int vip = 0, vcs = 0;
vjmp:
// このプログラムではコードセグメントは1つしかないので、vcsによる分岐命令は省略している.
switch(vip) {
case 0:
MOV8RI(AH, 0x02); // AH=02: 1文字出力.
MOV8RI(DL, 0x20);
LB(lp);
INT(0x21, 1); case 1: // int-no, vip.
INC16R(DX);
CMP8RI(DL, 0x7f);
JNE(lp);
MOV16RI(AX, 0x4c00);
INT(0x21, 2); case 2: ;
}
}
-この書き方のポイントは、INT命令のようなスタックにIP値を積む命令の場合、本当のIP値を積むのではなくvirtualなIPを積みます(C言語に変換して実行する場合)。同様にCSを積む場合は本当のCS値を積むのではなくvirtualなCSを積みます(同じくC言語に変換して実行する場合)。
-もちろんアセンブラや機械語に変換する場合は、こんなvip値は全く不要なので無視されます。
-C言語に変換する場合、CALL/RETはこうなります。以下はnear-call, near-retの例です。
#define CALL16(lb, vi) PUSH16I(vi); goto lb
#define RET16() vip = pop16(); goto vjmp
-関数ポインタのようにラベルの値を取得する必要がある時も、case命令を書いてvip値を定めます。
** (5) コード例
-再帰でフィボナッチ数を計算するやつを作ってみました。CALL/RETがちゃんと動くのかテストしたかったのです。
void casMain()
{
casMain_init;
ORG(0x100);
int vip = 0, vcs = 0;
vjmp:
// このプログラムではコードセグメントは1つしかないので、vcsによる分岐命令は省略している.
switch(vip) {
case 0:
MOV16RI(CX, 20);
CALL16(fib, 1); case 1:
printf("AX=%d\n", ax); // ズル.
return; // ズル.
LB(fib); // CXに引数を入れてCALL16すると、AXに結果を返す. AX以外のレジスタは破壊しない.
MOV16RR(AX, CX);
CMP16RI(CX, 2);
JB(skip);
PUSH16R(DX);
DEC16R(CX);
CALL16(fib, 2); case 2:
MOV16RR(DX, AX);
DEC16R(CX);
CALL16(fib, 3); case 3:
ADD16RR(AX,DX);
INC16R(CX);
INC16R(CX);
POP16R(DX);
LB(skip);
RET16();
}
}
-C言語化して動かしたら、うまくいきましたー。
-もうCALL16マクロの中にcaseの記述も入れちゃえばいいかもしれない。そうすれば見た目はすっきりする。
-もうCALL16マクロの中にcaseの記述も入れちゃえばいいかもしれないです。そうすれば見た目はすっきりします。
-しかしそれにしても、C言語ってすごく柔軟だなーって思います。他の言語でこんなことができるかどうか自信ないです。