a21_txt02_7
の編集
https://essen.osask.jp/?a21_txt02_7
[
トップ
] [
編集
|
差分
|
バックアップ
|
添付
|
リロード
] [
新規
|
一覧
|
単語検索
|
最終更新
|
ヘルプ
]
-- 雛形とするページ --
BracketName
EssenRev4
FormattingRules
FrontPage
Help
InterWiki
InterWikiName
InterWikiSandBox
K
MenuBar
PHP
PukiWiki
PukiWiki/1.4
PukiWiki/1.4/Manual
PukiWiki/1.4/Manual/Plugin
PukiWiki/1.4/Manual/Plugin/A-D
PukiWiki/1.4/Manual/Plugin/E-G
PukiWiki/1.4/Manual/Plugin/H-K
PukiWiki/1.4/Manual/Plugin/L-N
PukiWiki/1.4/Manual/Plugin/O-R
PukiWiki/1.4/Manual/Plugin/S-U
PukiWiki/1.4/Manual/Plugin/V-Z
RecentDeleted
SDL2_01
SandBox
WikiEngines
WikiName
WikiWikiWeb
YukiWiki
a21
a21_acl01
a21_bbs01
a21_challengers
a21_count
a21_edu01
a21_edu02
a21_edu03
a21_edu04
a21_edu05
a21_edu06
a21_edu07
a21_edu08
a21_edu09
a21_edu10
a21_edu11
a21_hlx000
a21_hlx001
a21_hlx001_1
a21_hlx001_2
a21_hlx001_3
a21_hlx002
a21_hlx002_1
a21_hlx003
a21_hlx003_1
a21_hlx004_1
a21_memo01
a21_opt
a21_opt02
a21_opt03
a21_p01
a21_special
a21_tl9a
a21_todo
a21_txt01
a21_txt01_10
a21_txt01_1a
a21_txt01_2
a21_txt01_2a
a21_txt01_2b
a21_txt01_3
a21_txt01_4
a21_txt01_5
a21_txt01_6
a21_txt01_6a
a21_txt01_7
a21_txt01_8
a21_txt01_8a
a21_txt01_9
a21_txt01_9a
a21_txt02
a21_txt02_10
a21_txt02_10a
a21_txt02_10b
a21_txt02_11
a21_txt02_11a
a21_txt02_12
a21_txt02_12a
a21_txt02_12b
a21_txt02_1a
a21_txt02_1b
a21_txt02_2
a21_txt02_2a
a21_txt02_3
a21_txt02_3a
a21_txt02_4
a21_txt02_4a
a21_txt02_5
a21_txt02_5a
a21_txt02_6
a21_txt02_6a
a21_txt02_6b
a21_txt02_6b_rev0
a21_txt02_6x
a21_txt02_7
a21_txt02_7a
a21_txt02_8
a21_txt02_8a
a21_txt02_9
a21_txt02_9a
a22_acl2_01
a22_acl2_02
a22_edu12
a22_intro01
a22_intro02
a22_intro03
a22_memman01
a22_memman02
a22_memman03
a22_memman04
a22_memman05
a22_memman06
a22_memman07
a22_memo01
a22_mingw_debug
a22_txt03
a22_txt03_1a
a22_txt03_1b
a22_txt03_2
a22_txt03_2a
a22_ufcs01
a23_bbs
a23_ec001
a23_ec002
a23_intro00
a23_intro000
a23_intro01
a23_intro02
a23_intro03
a23_intro04
a23_intro05
a23_intro06
a23_intro07
a23_intro08
a23_intro09
a23_intro10
a23_intro10wk1
a23_intro10wk2
a23_intro10wk3
a23_intro11
a23_intro12
a23_intro13
a23_intro13wk1
a23_intro14
a23_intro15
a23_intro16
a23_intro17
a23_intro17wk1
a23_intro18
a23_intro19
a23_intro90
a23_intro91
a23_neopixel1
a23_os01
a23_useSelfMade
a23_usm001
a23_usm002
a23_usm003
a23_usm004
a23_usm005
a23_usm006
a23_usm007
a23_usm008
a23_usm009
a24_AMap11
a24_AMapSim11
a24_AMemFile
a24_AMemMan
a24_aErrExit
a24_aFnv
a24_aOsFunc
a24_aQSort
a24_aXorShift32
a24_acl1T_doc01
a24_acl1Tiny
a24_acpp0
a24_buntan01
a24_cMin
a24_getTyp
a24_goodvalues
a24_idea001
a24_longdef
a24_memo01
a24_memo02
a24_osc20240310
a24_osc20241026
a24_picoLcd13
a24_picoTrain1
a24_programs
a24_raspberrypi01
a24_raspberrypi02
a24_schedule
a24_spc2tab
a24_tab2spc
a24_useSelfMade
a25_acl3
a25_buntan02
a25_buntan03
a25_buntan04
a25_buntan05
a25_kcas01
a25_kharc01
a25_kharc02
a25_kharc03
a25_kharc04
a25_kharc05
a25_kharc06
a25_kharcs1
a25_kharcs2
a25_kharcs3
a25_kharcs4
a25_kharcs5
a25_kharcs6
a25_kharcs7
a25_kharcs8
a25_kharcs9
aclib00
aclib01
aclib02
aclib03
aclib04
aclib05
aclib06
aclib07
aclib08
aclib09
aclib10
aclib11
aclib12
aclib13
aclib14
aclib15
aclib16
aclib17
aclib18
aclib19
aclib20
aclib21
aclib22
aclib23
aclib24
aclib25
aclib_bbs
arm64_01
avm0001
edu0001
edu0002
edu0003
esb02b_hrb
esb_dbg
esbasic0001
esbasic0002
esbasic0003
esbasic0004
esbasic0005
esbasic0006
esbasic0007
esbasic0008
esbasic0009
esbasic0010
esbasic0011
esbasic0012
esbasic0013
esbasic0014
esbasic0015
esbasic0016
esbasic0017
esbasic02a
esc0001
escm0001
essen_hist
esvm0001
esvm0002
esvm0003
esvm0004
esvm0005
esvm0006
esvm_i0
hh4a
idea0001
idea0002
idea0003
impressions
jck_0000
jck_0001
kawai
kbcl0_0000
kbcl0_0001
kbcl0_0002
kbcl0_0003
kbcl0_0004
kbcl0_0005
kbcl0_0006
kbcl0_0007
kclib1_0000
kclib1_0001
kclib1_0002
kclib1_0003
kclib1_0004
kclib1_0005
kclib1_0006
kclib1_0007
kclib1_0008
kclib1_0009
kclib1_0010
kpap0001
members
memo0001
osask4g
osask4g_r2
p20200311a
p20200610a
p20200610b
p20200624a
p20200711a
p20200716a
p20250813a
p20250813b
p20250813c
p20250815a
p20250903a
p20251006a
page0001
page0002
page0003
page0004
page0005
page0006
page0007
page0008
page0009
page0010
page0011
page0012
page0013
page0014
page0015
page0016
page0017
page0018
page0019
page0020
page0021
page0022
page0023
populars
seccamp
seccamp2019
sechack
sechack2019
seclang01
sh3_2020
sh3_2020_kw
sh3_2020_nk
sh3_2021_kw
sh3_2021_nk
sh3_2022_kw
sh3_2023_kw
sh3_2024_kw
sh3_2025_kw
sh3_kw_hist
termux001
termux002
text0001
text0001a
text0002
text0002a
text0003
text0004
text0005
text0006
text0006a
text0007
text0008
text0010
text0011
text0012
text0013
text0014
text0015
text0016
text0017
text0018
text0019
text0020
text0021
tl1c
tl2c
tl3c
tl3d
* 「10日くらいでできる!プログラミング言語自作入門」の続編#1-7 -(by [[K]], 2021.05.06) ** (1) はじめに -このテキストは、「10日くらいでできる!プログラミング言語自作入門」([[a21_txt01]])の続編にあたります。ですからこの続編テキストのスタート地点は772行の[[HL-9a>a21_txt01_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("%R_8b_%1m0; %R_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整数で書き込まれているのですが、それを参照させています。 --なお、関数の呼び出し規則については http://herumi.in.coocan.jp/prog/x64.html を参考にしました。 ---- -これですべてです。単純代入とprint命令だけならこれで十分だというわけです。 ** (6) 機械語を調べるためのリンク集 -https://www.wdic.org/w/SCI/ModR/M : mod R/Mについて -https://www.wdic.org/w/SCI/REX%E3%83%97%E3%83%AA%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%82%B9 : REXプリフィクスについて -http://ref.x86asm.net/coder64.html : x64の機械語の一覧 ** 次回に続く -次回: [[a21_txt02_7a]] *こめんと欄 #comment
タイムスタンプを変更しない
* 「10日くらいでできる!プログラミング言語自作入門」の続編#1-7 -(by [[K]], 2021.05.06) ** (1) はじめに -このテキストは、「10日くらいでできる!プログラミング言語自作入門」([[a21_txt01]])の続編にあたります。ですからこの続編テキストのスタート地点は772行の[[HL-9a>a21_txt01_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("%R_8b_%1m0; %R_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整数で書き込まれているのですが、それを参照させています。 --なお、関数の呼び出し規則については http://herumi.in.coocan.jp/prog/x64.html を参考にしました。 ---- -これですべてです。単純代入とprint命令だけならこれで十分だというわけです。 ** (6) 機械語を調べるためのリンク集 -https://www.wdic.org/w/SCI/ModR/M : mod R/Mについて -https://www.wdic.org/w/SCI/REX%E3%83%97%E3%83%AA%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%82%B9 : REXプリフィクスについて -http://ref.x86asm.net/coder64.html : x64の機械語の一覧 ** 次回に続く -次回: [[a21_txt02_7a]] *こめんと欄 #comment
テキスト整形のルールを表示する