kcas #1
(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); // int-no, vip.
case 1:
INC16R(DX);
CMP8RI(DL, 0x7f);
JNE(lp);
MOV16RI(AX, 0x4c00);
INT(0x21, 2);
case 2:
;
}
}
(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();
}
}