「10日くらいでできる!プログラミング言語自作入門」の続編#1-7

  • (by K, 2021.05.06)

(1) はじめに

  • このテキストは、「10日くらいでできる!プログラミング言語自作入門」(a21_txt01)の続編にあたります。ですからこの続編テキストのスタート地点は772行のHL-9aになります。
  • このシリーズでは、言語に新規の命令を追加することは最小限に抑えて、主にJITコンパイラ化や普通のコンパイラへの改造がメインです。
  • 言語に独自の機能を加えていって言語を「進化」させていく話は、「a21_txt03」でやる予定です。そこでは言語がどうすれば便利になるかを考えていきます。

(2) HL-17

  • ということで、HL-17について説明したいと思います。これはHL-11のx64版に相当します。
  • HL-17の基本方針はこうです。
    • [1]compile()関数では、内部コード出力をやめて、代わりにx64の機械語を出力する。
    • [2]exec()関数は不要なので削除。
    • [3]機械語を出力するにあたって、putIc()のままでは全然便利ではないので、putIcX64()を新規に作る。
    • [4]それにともない、putIc()関数は期待通りには動かなくなるので、これを呼び出したらエラー終了するようにしておく(将来的にはこの関数は削除する)。
    • [5]とりあえず、単純代入命令とprint命令だけputIcX64()に対応させる。残りは後回し。
  • こうすることで、HL-9aをJITコンパイラ型の言語に改造します。そうすると何がよくなるのかというと、実行速度がうんと速くなるはずなのです。・・・それだけです。
  • この改造をすると、言語はCPUに依存するようになります。x64以外ではこのプログラムは動きません。それがデメリットです。・・・またもし自力で改造する場合は、機械語に対する知識も必要になります。今まではC言語で普通に書くだけでうまくできたので、それと比べればハードルは高いです。
  • しかしそれでも、やるだけの価値はあります。それほど高速になります。
  • 説明が前後していますが「JITコンパイラ」というのは「Just-In-Time コンパイラ」のことで、ソースファイルからコンパイルして実行ファイルを作る普通のコンパイラとは異なり、インタプリタで命令の実行を指示された瞬間に高速にコンパイルして実行するという仕組みのことです。ユーザからはコンパイル動作は全く意識されず、ただの速いスクリプト言語に見えます。
#include <acl.c>

typedef unsigned char *String;	// こう書くと String は unsigned char * の代用になる.

int loadText(String path, String t, int siz) → HL-4と同じなので省略

///////////////////////////////////////////////////////////////////////////////

#define MAX_TC  1000 // トークンコードの最大値.
String ts[MAX_TC + 1]; // トークンの内容(文字列)を記憶.
int tl[MAX_TC + 1]; // トークンの長さ.
unsigned char tcBuf[(MAX_TC + 1) * 10]; // トークン1つ当たり平均10バイトを想定.
int tcs = 0, tcb = 0;

AInt var[MAX_TC + 1];	// 変数.

int getTc(String s, int len) → HL-8aと同じなので省略

///////////////////////////////////////////////////////////////////////////////

int isAlphabetOrNumber(unsigned char c) → HL-2と同じなので省略

int lexer(String s, int tc[]) → HL-9aと同じなので省略

int tc[10000];	// トークンコード.

enum { TcSemi = 0, TcDot, TcWiCard, Tc0, Tc1, Tc2, Tc3, Tc4, Tc5, Tc6, Tc7, Tc8, TcBrOpn, TcBrCls, TcSqBrOpn, TcSqBrCls, TcCrBrOpn, TcCrBrCls,
    TcEEq, TcNEq, TcLt, TcGe, TcLe, TcGt, TcPlus, TcMinus, TcAster, TcSlash, TcPerce, TcAnd, TcShr, TcPlPlus, TcEqu,
    TcComma, TcExpr, TcExpr0, TcTmp0, TcTmp1, TcTmp2, TcTmp3, TcTmp4, TcTmp5, TcTmp6, TcTmp7, TcTmp8, TcTmp9,
    TcSubPrint, TcSubTime, TcSubRgb8, TcSubXorShift, TcSubGetPix, TcSubF16Sin, TcSubF16Cos, TcSubInkey, TcSubPrints, TcSubSetPix0,
    TcSubFilRct0, TcSubDrwStr0, TcSubGprDec, TcSubOpnWin, TcSubWait, TcSubBitBlt, TcSubAryNew, TcSubAryInit };

char tcInit[] = "; . !!* 0 1 2 3 4 5 6 7 8 ( ) [ ] { } == != < >= <= > + - * / % & >> ++ = , !!** !!*** _t0 _t1 _t2 _t3 _t4 _t5 _t6 _t7 _t8 _t9 "
    "sub_print sub_time sub_aRgb8 sub_xorShift sub_aGetPix sub_f16Sin sub_f16Cos sub_aInkey sub_prints sub_aSetPix0 "
    "sub_aFilRct0 sub_aDrwStr0 sub_gprDec sub_opnWin sub_aWait sub_bitblt sub_aryNew sub_aryInit";

///////////////////////////////////////////////////////////////////////////////

int phrCmp_tc[32 * 100], ppc1, wpc[9], wpc1[9]; // ppc1:一致したフレーズの次のトークンをさす, wpc[]:ワイルドカードのトークンの場所をさす.

int phrCmp(int pid, String phr, int pc) → HL-7と同じなので省略

/////////////////////////////////////////////////////////////////////////////// → ここまではHL-9aと全く同じ、ここから改造が始まる.

#define ABI_MSW64   1 // Windows.
#define ABI_SYSV64  0 // Linuxなど.

typedef AInt *IntP; // こう書くと IntP は AInt * の代わりに使えるようになる.

enum { OpCpy = 0, OpCeq, OpCne, OpClt, OpCge, OpCle, OpCgt, OpAdd, OpSub, OpMul, OpDiv, OpMod, OpAnd, OpShr, 
    OpAdd1, OpNeg, OpGoto, OpJeq, OpJne, OpJlt, OpJge, OpJle, OpJgt, OpLop, OpPrint, OpTime, OpEnd,
    OpPrints, OpAryNew, OpAryInit, OpArySet, OpAryGet, OpOpnWin, OpSetPix0, OpM64s, OpRgb8, OpWait,
    OpXorShift, OpGetPix, OpFilRct0, OpPrm, OpF16Sin, OpF16Cos, OpInkey, OpDrwStr0, OpGprDec, OpBitBlt };

unsigned char *ic, *icq; // ic[]:内部コード、icq:ic[]への書き込み用ポインタ.

void putIc(int op, IntP p0, IntP p1, IntP p2, IntP p3)  // 移行中の間だけ、以下の形で残しておく.
{
    printf("putIc: error\n");
    exit(1);
}

int getHex(int c) // 16進数に使える文字ならそれを0~15の数に変換、そうでなければ-1を返す関数.
{
    if ('0' <= c && c <= '9') return c - '0';
    if ('a' <= c && c <= 'f') return c - 'a' + 10;
    if ('A' <= c && c <= 'F') return c - 'A' + 10;
    return -1;
}

int get32(unsigned char *p)
{
    return p[0] + p[1] * 256 + p[2] * 65536 + p[3] * 16777216;
}

void put32(unsigned char *p, AInt i)
{
    p[0] =  i        & 0xff; // 1バイト目に、ビット0~7の内容を書き込む.
    p[1] = (i >>  8) & 0xff; // 2バイト目に、ビット8~15の内容を書き込む.
    p[2] = (i >> 16) & 0xff; // 3バイト目に、ビット16~23の内容を書き込む.
    p[3] = (i >> 24) & 0xff; // 4バイト目に、ビット24~31の内容を書き込む.
}

unsigned char *putIcX64_rex;

void putIcX64_sub(String s, IntP a[])
{
    int i, j, k;
    for (i = 0; s[i] != 0; ) {
        if (s[i] == ' ' || s[i] == '\t' || s[i] == '_' || s[i] == ':' || s[i] == ';') {
            i++;
        } else if (getHex(s[i]) >= 0 && getHex(s[i + 1]) >= 0) { // 16進数2桁.
            *icq = getHex(s[i]) * 16 + getHex(s[i + 1]);
            i += 2;
            icq++;
        } else if (s[i] == '%') {
            if (s[i + 1] == 'R') {
                putIcX64_rex = icq;
                *icq = 0x48;
                icq++;
                i += 2;
                continue;
            }
            j = s[i + 1] - '0';
            if (s[i + 2] == 'm') {	// mod r/m.
                k = s[i + 3] - '0';
                if (s[i + 3] >= 'a') { // もしかしたらこの記述は最後まで出番がないかも.
                    k = s[i + 3] - 'a' + 10;
                }
                *icq = 0x85 + (k & 7) * 8;
                if (k >= 8) {
                    *putIcX64_rex |= 4;
                }
                put32(icq + 1, (a[j] - var) * 8);
                icq += 5;
                i += 4;
                continue;
            }
            if (s[i + 2] == 'i') {	// int.
                put32(icq, (AInt) a[j]);
                icq += 4;
            }
            if (s[i + 2] == 'c') {	// char.
                *icq = (AInt) a[j];
                icq++;
            }
            if (s[i + 2] == 'q') {	// long long (qword).
                put32(icq,      (AInt) a[j]);
                put32(icq + 4, ((AInt) a[j]) >> 32);
                icq += 8;
            }
            i += 3;
        } else {
            printf("putIcX64: error: '%s'\n", s);
            exit(1);
        }
    }
}

void putIcX64(String s, IntP p0, IntP p1, IntP p2, IntP p3)  // ic[]へ簡単に書き込むための便利関数.
{
    IntP a[4];
    a[0] = p0;
    a[1] = p1;
    a[2] = p2;
    a[3] = p3;
    putIcX64_sub(s, a);
}

///////////////////////////////////////////////////////////////////////////////

char tmp_flag[10]; // 一時変数の利用状況を管理.

int tmpAlloc() → HL-7と同じなので省略

void tmpFree(int i) → HL-7と同じなので省略

///////////////////////////////////////////////////////////////////////////////

void sub_print(AInt i)
{
    printf("%d\n", (int) i);
}

void initTcSub()
{
    var[TcSubPrint] = (AInt) sub_print;
}

///////////////////////////////////////////////////////////////////////////////

int epc, epc1;	// exprのためのpcとpc1.

int exprSub(int priority);	// exprSub1()が参照するので、プロトタイプ宣言.
int expr(int j);
int phrCmpPutIc(int pid, String phr, int pc, int *pi, int lenExpr, int op, int *err);

int exprSub1(int i, int priority, int op) → HL-7と同じなので省略

int exprSub(int priority) → HL-9と同じなので省略

int expr(int j) → HL-7と同じなので省略

///////////////////////////////////////////////////////////////////////////////

enum { IfTrue = 0, IfFalse = 1 };

void ifgoto(int i, int not, int label) → HL-8と同じなので省略

int tmpLabelNo;

int tmpLabelAlloc() → HL-8と同じなので省略

#define BInfSiz		10

int binf[BInfSiz * 100], bd, lbd; // binf:block-info, bd:block-depth, lbd:loop-block-depth

enum { BlkIf = 1, BlkFor, BlkMain }; // BlkMainをHL-9aで追加.
enum { IfLabel0 = 1, IfLabel1 };
enum { ForLopBgn = 1, ForCont, ForBrk, ForLbd0, ForWpc01, ForWpc11, ForWpc02, ForWpc12 };

int phrCmpPutIc(int pid, String phr, int pc, int *pi, int lenExpr, int op, int *err)  // 移行中の間だけ、以下の形で残しておく.
{
    if (phrCmp(pid, phr, pc)) {
        printf("phrCmpPutIc: error\n");
        exit(1);
    }
    return 0;
}

int phrCmpPutIcX64(int pid, String phr, int pc, int *pi, int lenExpr, int sub, int *err)
{
    int e[9], i;
    if (phrCmp(pid, phr, pc)) {
        e[0] = e[1] = e[2] = e[3] = e[4] = e[5] = e[6] = e[7] = e[8] = 0;
        for (i = 0; i < lenExpr; i++) {
            e[i] = expr(i);
        }
        #if (ABI_MSW64 != 0)
            if (lenExpr >=  1) { putIcX64("%R_8b_%0m1;", &var[e[0]], 0, 0, 0); } // RCX=arg[0].
            if (lenExpr >=  2) { putIcX64("%R_8b_%0m2;", &var[e[1]], 0, 0, 0); } // RDX=arg[1].
            if (lenExpr >=  3) { putIcX64("%R_8b_%0m8;", &var[e[2]], 0, 0, 0); } // R08=arg[2].
            if (lenExpr >=  4) { putIcX64("%R_8b_%0m9;", &var[e[3]], 0, 0, 0); } // R09=arg[3].
            for (i = 4; i < lenExpr; i++) {
                putIcX64("%R_8b_%0m0; %R_89_44_24_%1c;", &var[e[i]], (IntP) ((AInt) i * 8), 0, 0);
            }
        #elif (ABI_SYSV64 != 0)
            if (lenExpr >=  1) { putIcX64("%R_8b_%0m7;", &var[e[0]], 0, 0, 0); } // RDI=arg[0].
            if (lenExpr >=  2) { putIcX64("%R_8b_%0m6;", &var[e[1]], 0, 0, 0); } // RDX=arg[1].
            if (lenExpr >=  3) { putIcX64("%R_8b_%0m2;", &var[e[2]], 0, 0, 0); } // R08=arg[2].
            if (lenExpr >=  4) { putIcX64("%R_8b_%0m1;", &var[e[3]], 0, 0, 0); } // R09=arg[3].
            if (lenExpr >=  5) { putIcX64("%R_8b_%0m8;", &var[e[4]], 0, 0, 0); } // R08=arg[4].
            if (lenExpr >=  6) { putIcX64("%R_8b_%0m9;", &var[e[5]], 0, 0, 0); } // R09=arg[5].
            for (i = 6; i < lenExpr; i++) {
                putIcX64("%R_8b_%0m0; %R_89_44_24_%1c;", &var[e[i]], (IntP) ((AInt) (i - 6) * 8), 0, 0);
            }
        #endif
        putIcX64("%R_ff_%0m2;", &var[sub], 0, 0, 0); // 間接call.
        for (i = 0; i < lenExpr; i++) {
            if (e[i] < 0) {
                *err = -1;
            }
            tmpFree(e[i]);
        }
        if (pi != 0) {
            *pi = tmpAlloc();
            putIcX64("%R_89_%0m0;", &var[*pi], 0, 0, 0); // RAXの値を書き込む.
        }
        return 1;
    }
    return 0;
}

///////////////////////////////////////////////////////////////////////////////

int compile(String s)
{
    int pc, pc1, i, j;
!   unsigned char *icq1, *icp;
    pc1 = lexer(s, tc);
    tc[pc1++] = TcSemi;	// 末尾に「;」を付け忘れることが多いので、付けてあげる.
    tc[pc1] = tc[pc1 + 1] = tc[pc1 + 2] = tc[pc1 + 3] = TcDot;	// エラー表示用のために末尾にピリオドを登録しておく.
    icq = ic;
+   putIcX64("41_57; 41_56; 41_55; 41_54; 41_53; 41_52;", 0, 0, 0, 0);
+   putIcX64("41_51; 41_50; 57; 56; 55; 54; 53; 52; 51; 50;", 0, 0, 0, 0);
+   putIcX64("%R_81_ec_f8_01_00_00; %R_bd_%0q;", var, 0, 0, 0); // RBP = var.
    for (i = 0; i < 10; i++) {
        tmp_flag[i] = 0;
    }
    tmpLabelNo = 0;
    bd = lbd = 0;
    for (pc = 0; pc < pc1; ) { // コンパイル開始.
        int e0 = 0, e2 = 0;
        if (phrCmp( 1, "!!*0 = !!*1;", pc)) { // 単純代入.
!           putIcX86("8b_%1m0; 89_%0m0;", &var[tc[wpc[0]]], &var[tc[wpc[1]]], 0, 0);
        } else if (phrCmp(10, "!!*0 = !!*1 + 1; if (!!*2 < !!*3) goto !!*4;", pc) && tc[wpc[0]] == tc[wpc[1]] && tc[wpc[0]] == tc[wpc[2]]) {
        (中略)
!       } else if (phrCmpPutIcX86(4, "print !!**0;", pc, 0, 1, sub_print, &e0)) {
        (中略)
    }
    if (bd > 0) {
        printf("block nesting error (bd=%d, lbd=%d, pc=%d, pc1=%d\n", bd, lbd, pc, pc1);
        return -1;
    }
+   putIcX64("%R_81_c4_f8_01_00_00;", 0, 0, 0, 0);
+   putIcX64("58; 59; 5a; 5b; 5c; 5d; 5e; 5f; 41_58; 41_59;", 0, 0, 0, 0);
!   putIcX64("41_5a; 41_5b; 41_5c; 41_5d; 41_5e; 41_5f; c3;", 0, 0, 0, 0);
    icq1 = icq;
    // ここにあった「goto先の設定」はいったん全部削除.
    return icq1 - ic;
err:
    printf("syntax error : %s %s %s %s\n", ts[tc[pc]], ts[tc[pc + 1]], ts[tc[pc + 2]], ts[tc[pc + 3]]);
    return -1;
}

// このあたりにあったexec()関数の記述はすべて削除.
// ついでに変数winの宣言も削除.

int run(String s)
{
    if (compile(s) < 0)
        return 1;
+   void (*func)() = (void (*)()) ic;
!   func();
    return 0;
}

// 以下のmallocRWX()はWindows用の記述. Linux系のOSの場合は本文中で説明します.

#include <windows.h>

void *mallocRWX(int siz)
{
    return VirtualAlloc(0, siz, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
}

///////////////////////////////////////////////////////////////////////////////

void aMain()
{
    unsigned char txt[10000];
    int i;
+   ic = mallocRWX(1024 * 1024); // 1MB
    lexer(tcInit, tc);
+   initTcSub();
    if (aArgc >= 2) {
    (中略)
}
  • プログラムは741行になりました。このHL-17では、printと単純代入命令だけが使えます。代入以外の演算は一切できません(それはHL-17aでやります)。
    >print 1
    1
    
    >print 2
    2
    
    >a=4
    
    >print a
    4
  • とてもつまらなくなったのではありますが、しかしこれは全部exec()なしで実現していて、自分の入力がx64の機械語になって実行されているのだと思うと、少し感動します。

(3) HL-17に関する重要な説明

  • このHL-17は、x64の64bitモード専用で書かれています。したがって、gccでコンパイルする場合は、64bit対応のコンパイラで、-m64を付けてコンパイルしなければいけません。他のコンパイラでも、もしモードを指定しなければいけない場合は、64bitを指定してください。
  • そしてこれはHL-11の説明にも書いたことですが、x64の機械語はx86の機械語よりも難しいです。だから少しだけ覚悟してください(笑)。

(4) 今回の改造のあらまし

  • 今回からic[ ]に機械語を入れて実行することになるのですが、近年のOSはセキュリティ対策のため、適当な配列に正しい機械語を書き込んで準備しても、それを実行することができません(データ実行防止機能のためです)。
  • しかしこれができなければ、そもそもJITコンパイラなんて実現できなくなってしまいます。ということで、それぞれのOSは、実行可能な配列を確保するための特別な手続きを用意しています。
  • Windowsの場合は、上記のプログラムのように
    #include <windows.h>
    
    void *mallocRWX(int siz)
    {
        return VirtualAlloc(0, siz, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    }
  • とすることで可能になります。
  • LinuxなどのPOSIX系のOSでは、以下のようにやればできます。
    #include <unistd.h>
    #include <sys/mman.h>
    
    void *mallocRWX(int siz)
    {
        return mmap(0, siz, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    }
  • HL-17ではこのmallocRWX()を呼び出して、返値をicに代入して、1MBのic[ ]を準備しています(aMain()の中でこの処理をやっています)。
  • ic[ ]の中に機械語を書き込んだ後は、
        void (*func)() = (void (*)()) ic;
        func();
  • これで呼び出しています(これはrun()関数の中に書いてあります)。この1行目は変数funcの宣言をして、初期値としてicを代入しています。funcは関数ポインタ型の変数です。すごくわかりにくい書き方をするのですが、これがC言語での書き方なので、それはひとまずそういうものなんだなということで許してください。
  • 2行目はただの関数呼び出しです。こうすることで、ic[ ]に書かれた機械語が普通の関数のように実行されるのです。
  • ic[ ]に適切な機械語を書き込むのは、compile()関数の仕事です。・・・以上のような構成で、HL-17は作られています。
  • なお、ic[ ]に機械語を書き込む際には、1バイトずつ書き込むほうが便利なので、ic[ ]の型も符号なしのchar型に変更しています。

(5) 生成している機械語の説明(putIcX64()やphrCmpPutIcX64()の説明も含む)

  • ここを読んでいる多くの人は、おそらくx64の機械語なんて知らないでしょう。普通にプログラムしているだけならそんなことは知らなくていいことです。

  • まず大事なこととして、x64ではWindowsかそれ以外かで、関数の呼び出し規則が異なります。それに合わせて機械語を生成しないとうまくいきません。ということで、どちらの方法なのかを、
    #define ABI_MSW64   1 // Windows.
    #define ABI_SYSV64  0 // Linuxなど.
  • のところで選んでください(上記はWindowsの場合になっています)。
  • compile()では、ic[ ]の冒頭に、
        putIcX64("41_57; 41_56; 41_55; 41_54; 41_53; 41_52;", 0, 0, 0, 0);
        putIcX64("41_51; 41_50; 57; 56; 55; 54; 53; 52; 51; 50;", 0, 0, 0, 0);
        putIcX64("%R_81_ec_f8_01_00_00; %R_bd_%0q;", var, 0, 0, 0);
  • を書きこんでいます。また末尾には、
        putIcX64("%R_81_c4_f8_01_00_00;", 0, 0, 0, 0);
        putIcX64("58; 59; 5a; 5b; 5c; 5d; 5e; 5f; 41_58; 41_59;", 0, 0, 0, 0);
        putIcX64("41_5a; 41_5b; 41_5c; 41_5d; 41_5e; 41_5f; c3;", 0, 0, 0, 0);
  • を書き込んでいます。・・・まずはこのあたりから説明します。
  • まず命令「50」~「57」は、1バイトの命令で、RAX~RDIの8つのレジスタの値をスタックに書き込む命令です。ここで保存した値は、末尾の命令「58」~「5f」ですべて読み込まれて、レジスタの値が元通りになります。またこれらの命令には「41」というプリフィクス(形容詞みたいなもの)を付けることができて、そうすると対象のレジスタがR08~R15に変わります。
  • ・・・こうすることで、プログラム内では16個のレジスタを自由に使えるようになります。どんなに値がおかしくなっても、戻せる保証があるので心配いりません(とはいえ、RSPレジスタは自由にはできないので、実際に自由に使うのはそれ以外の15個のレジスタだけですが)。
  • 命令「%R_81_ec_f8_01_00_00;」は7バイトの命令で、RSP=RSP-504;を計算させる命令です。これで関数呼び出しのための引数受け渡し領域を確保しています(こんなにたくさんは必要ないかもしれませんが、ひとまず大きめにしてあります)。これに対応しているのが「%R_81_c4_f8_01_00_00;」です。こちらはRSP=RSP+504;計算させる命令で、上記で確保した領域を解放するために使っています。・・・ここで出てきた「%R」という謎の記述ですが、現段階では「48」というプリフィクスの代わりだと思っていれば間違いありません。これはREXプリフィクスというもので、多くの命令ではこれを付けないと64bit演算にならないのです(先の「50」~「5f」はREXがなくても64bit命令です)。
  • そして「%R_bd_%0q;」はRBPレジスタに64bit定数を代入するための命令です(%0qの部分が64bit定数になります)。この代入はのちに%mでvar[ ]変数の中身にアクセスする際に必要になります。・・・最後の「c3」はreturn;命令です。
  • ここでputIcX64()関数についても説明します。最初に文字列で書き込みたい機械語を記述します。16進数で書けばいいだけなのですが、間にスペースやアンダスコアやコロンやセミコロンを自由に混ぜることができます(どれも単に無視されます)。そのあとに4つのゼロがありますが、これは文字列の中に%拡張命令を書いた時に参照されるデータを置くところで、%拡張を使わないときは単に0を4つ並べておきます(何を書いても無視されます)。

  • 単純代入命令では、
        putIcX64("%R_8b_%1m0; %R_89_%0m0;", &var[tc[wpc[0]]], &var[tc[wpc[1]]], 0, 0);
  • という機械語を使っています。ちなみにここはHL-9aでは
        putIc(OpCpy,  &var[tc[wpc[0]]], &var[tc[wpc[1]]], 0, 0);
  • になっていました。第二引数以降は完全に同じになっています。違いが少ないほうが理解が早まるだろうと思って頑張りました!
  • 命令「%R_8b」と「%R_89」はどちらもアセンブラではMOV命令と呼ばれているもので、メモリから64bitのデータを読み込んでレジスタにしまったり、レジスタの64bitのデータをメモリに書き込んだり、レジスタからレジスタに64bitのデータをコピーしたりすることができます。8bの場合、「メモリ→レジスタ」で、89が「レジスタ→メモリ」になります。「レジスタ→レジスタ」の場合は、8bでも89でもどっちでもOKです。
  • 次の%1m0の意味するところは、まず1で「追加引数の[1]を参照しろ、と指示しています。つまりここでは「&var[tc[wpc[1]]]」を選んだことになります。次にmです。これで8b命令のためのパラメータ(機械語用語ではオペランド)を生成することになります。
  • 最後の0は、オペランド内の第二引数欄に0を指定せよ、という意味です。
  • 8b命令や89命令では、命令コードの直後に「mod r/mバイト」と呼ばれる1バイトを必ず置くことになっています。この1バイトでオペランド内容を記述するのです。この1バイトで指定できるオペランドは2つあって、第一オペランドではメモリかレジスタのどちらかを指定できます。第二オペランドではレジスタしか指定できません(命令によっては第二オペランドで0~7の定数を指定することもあります)。メモリを指定した場合は、さらに何バイトかの追加パラメータが後続する場合もあります。
    • 第一オペランドでvar[ ]変数にアクセスしたい場合、HL-17は[RBP+?]という機械語を生成します。?は32bitの整数です。
  • この例では第二オペランドは0なので、RAXレジスタを指定したことになります。
    • このmod r/mについての詳しいことは、たとえば https://www.wdic.org/w/SCI/ModR/M に書いてありますが、しかしこれはいきなり見てもよくわからないと思います・・・。
  • ということでまとめると、最初の8b命令で、 RAX = var[tc[wpc[1]]]; を実行します。次の89命令で、 var[tc[wpc[0]]] = RAX; を実行します。だから変数から変数へ単純代入をしたことになるわけです。

  • 次はprint命令です。phrCmpPutIcX64()関数によって、こんな感じに機械語が生成されます(Windowsの場合)。
    putIcX64("%R_8b_%0m1;", &var[e[0]], 0, 0, 0); // RCX=arg[0].
    putIcX64("%R_ff_%0m2", &var[TcSubPrint], 0, 0, 0); // 間接call.
  • 1行目で、print命令の引数をRCXに読み込みます。
  • 2行目の%R_ff_%m/2命令は、アセンブラでは間接CALL命令と言われているものです。関数を呼び出します。var[TcSubPrint]にsub_printの関数の場所が64bit整数で書き込まれているのですが、それを参照させています。

  • これですべてです。単純代入とprint命令だけならこれで十分だというわけです。

(6) 機械語を調べるためのリンク集

次回に続く

こめんと欄


コメントお名前NameLink

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2021-05-29 (土) 11:35:44 (120d)