Intel 8086から現代のCore iシリーズまで、脈々と続くx86アーキテクチャ。実は、誕生1978年と私と同い年。RISC-Vのシンプルさと合わせて引き合いに出される古の命令「AAA」を動かしてみました。


参考図書「8086マシン語秘伝の書 単行本 – 1990/10 by 日高徹氏、青山学氏
MSXでマシン語を使ってみたくて買った「Z80マシン語秘伝の書」の8086版! 16bit CPUである8086向けに、変更、セグメントなどx86ならではの項目が追加されている。Z80同様、古典と思いきや、普通に現代のMacでも普通に使えてしまうことに驚きます。

AAA命令もちゃんと載ってました!
2進法/16進法が基本のコンピューターと、10進法が基本の人をつなぐ命令の一つ。0から15までの数を2byte2桁表現にしてくれる便利命令でした。(src on GitHub)

_main: mov eax, 0 main_loop: push eax clc ; reset carry flag AAA call putnumhex pop eax inc eax cmp eax, 16 jne main_loop mov al, 0 ret

こちら実行結果

0000 0001 0002 0003 0004 0005 0006 0007 0008 0009 0100 0101 0102 0103 0104 0105

こちら0から15までをAXレジスタにセットして、AAA命令を通した結果です。確かに2byteになってくれると表示しやすくて便利です。

このような基本的な部分は一度作ってモジュール化してしまえば、再度作る必要はないので、専用の命令はリソースの無駄になります。 実際、x86アーキテクチャでも64bit環境ではAAA命令は削除されているので、上記プログラムは32bitモード(-f macho32)でコンパイルしています。(syscallの呼び出し方も違うので注意!)


こちら、8086マシン語秘伝の書から、144倍するという定数による掛け算を分解してする高速化するテクニック(main.s src on GitHub

SHL AX, 1 SHL AX, 1 SHL AX, 1 SHL AX, 1 MOV BX, AX SHL AX, 1 SHL AX, 1 SHL AX, 1 ADD AX, BX

144倍=128倍+16倍であることを利用して、シフトと足し算命令を使って実現しています。8086には掛け算命令MULがあるにも関わらずなぜこのテクニックが必要だったか?

秘伝の書記載の仕様によると、16bitの掛け算(MUL)にかかるクロック数はなんと118〜133。1bitシフト(SHL)も9クロックかかるとはいえ圧倒的に速いわけです。 現代CPUは掛け算も足し算も1クロックで終了するので、このテクニック自体は不要です。

そういえば、なぜわざわざ1bitずつシフトしているのでしょう?

SHL AX, 4 MOV BX, AX SHL AX, 3 ADD AX, BX

と書けばもっと短く、高速です。実は、この元になったZ80マシン語秘伝の書でも同じ内容があって、Z80には1bitシフトしかないので、ベタ移植したままになってしまったと想像。(追記、・・・違いました。8086にはイミディエイトのシフトは1限定。上記は80186から加えられた命令でした。 thanks! > @fujitanozomu

MOV CL, 4 SHL AX, CL MOV BX, AX MOV CL, 3 SHL AX, CL ADD AX, BX

8086命令を使って、このように書くこともできますが、2クロックで終わる1bitシフトと比較し、CLを使ったシフトは 8+4N クロックと遅く、速度を取るなら秘伝書記載の通りが正解でした。

コンピューターの特性を知り、いかに活かすか、今でも大事な基本ですね!
普段使っているCPUに近づくマシン語の世界、こちらのサンプルや入門をどうぞ!(x86asm src on GitHub

links
- 日本人が創ったCPUの歴史、MacのCPU Intel 64 マシン語はじめのいっぽ
- ハンドアセンブルで高速計算! RISC-V、RV32ICエミュレーターのC言語実装
- はじめてのマシン語 - IchigoJamではじめるArmマシン語その1

人気のArmマシン語入門の「つぎのいっぽ」ハンドアセンブルをRISC-Vでやってみました。


参考図書「RISC-V原典 オープンアーキテクチャのススメ | デイビッド・パターソン, アンドリュー・ウォーターマン, 成田 光彰
著者、デイビッド・パターソン氏は、RISC生みの親!

せっかくなのでハンドアセンブルしやすく実行効率の良い、16bitの圧縮命令拡張RV32Cを使いたいので、以前使用したエミュレーターにC拡張を勉強ついでに追加実装。(rv32emuの元、TinyEMUはC対応してます)

R11=0 R11+=R10 R10-=1 IF R10 GOTO -2 R10=R11 RET

RISC-VのC言語呼び出し規約では、R10が第一引数で返り値となり、R11も一時レジスタとして使用可能なので、R10/R11を使用。C拡張の命令のみを使ってasm15r表記で作成。

比較のためにこちらがArm Cortex-M0用、asm15表記ですが、ほぼ同じですね。

R1=0 R0=R0+R1 R0=R0-1 IF !0 GOTO -2 R0=R1 RET

RISC-Vにはフラグがない代わりに分岐命令で比較が可能です(比較した結果を0か1かで代入する命令でキャリーフラグを代用できます)。 足りない命令は、32bit命令を使えばOKなので、割り切ったコンパクトな設計が美しい。

RV32C RISC-Vマシン語表」を見ながらハンドアセンブルします。

0b0010010110000001, // R11=0 0b1001010110101010, // R11+=R10 0b0001010101111101, // R10-=1 0b1111110101110101, // IF R10 GOTO -2 0b1000010100101110, // R10=R11 0b1000000010000010, // RET

こちらを emu-rv32i-test-c.c で、仮想RAMに書き込んで、実行!
繰り返し足し算するプログラムが動きました!(src on GitHub


ArmのThmubと違って、C拡張はハードウェアで実装する別名的扱い。 よく使われる命令に絞って実装されており、32bit命令と併用できるのが特徴。たった400ゲートで実装できると言うだけあって、C言語での実装も約200行とコンパクト。(関数 convert_insn_from_c / src on GitHub

16bit命令を使うことで30%ほど容量が削減できて、ArmのThumbとほぼ近いサイズくらいになります。レジスタを複数PUSHするなどの命令を削減したため、より小さくはなりませんが、ハードウェアコストや実行性能にメリットあり。後発優位でもありますが、この辺りのバランス感覚が気持ちいい。

64bit時代にプログラムの容量削減に意味はあるのか?大有りなのです。メモリとのアクセスは時間がかかるので、何段ものキャッシュを使って実行効率を上げている現代CPU、命令がコンパクトになるとその分キャッシュヒット率が向上し、速く動作するわけです。実行時間=電力消費量=コストなので、省エネ化にもつながります。

RISC-V原典、一番おもしろかったのはV(ベクトル)拡張。CPU+GPUの歴史が変わりそう!V実装のフィックスと実CPUの広まり、大いに楽しみです。
(参考、RISC-Vベクトル拡張について解説する - Fixstars Tech Blog /proc/cpuinfoArm64のSIMD

理研のスパコンに使われたり、格安ハードが登場した2019年のRISC-V。今年もかなり伸びそうな予感のCPU、RISC-V(リスク・ファイブ)。RISC-V版Androidや、RISC-V版ノートパソコンの発売もあるかも?

RISC-Vは、由緒正しいハーバード大生まれのRISCアーキテチャーの第五世代。ライセンスフリーで自由に利用できるオープンかつ時代に合わせたチューニングによるハイパフォーマンスが特徴です。 以前32bit版で紹介しましたが、短くシンプルな16bitの縮小命令RV32Cを使って、Arm Cortex-M0学習用のasm15をRISC-Vに対応させたasm15rを設計。

そもそも、コンピューターとは何でしょう?
コンピューターとは、膨大な数を記憶し、正確かつ超高速に計算するものです。

どう記憶しどう計算するかを指示する数、それがマシン語。 CPUが違えばマシン語が違います。各メーカーによって系統があり、具体的なCPUによって使える命令が異なりますが、基本的はどれも一緒です。


RV32C RISC-Vマシン語表 (asm15r)
こちらまとめたRISC-Vマシン語です(抜粋)
RISC-Vのラインナップは、32bit/64bit/128bitの大きく三種類。今回は入門しやすい32bitで縮小命令、掛け算割り算命令対応版をセレクト。

32bitコンピューターでの記憶は大きく2種類。数個から数十個ある32bit(4byte)のレジスタと呼ばれるものと、数KBから数GBまでのメモリ(RAMやROM)と呼ばれるもので記憶します。 RISCアーキテクチャでは計算はレジスタを使って行い、メモリとレジスタとのやり取りは別の命令で行います。


こちらがメモリとレジスタをやりとりする命令。

ここでRISC-Vを使った超シンプルなRISC-V 32bitコンピューターを仮定します。
1. メモリはアドレス0から始まる32byteのROM(16bitずつ16セットのスイッチで0/1切り替え可能、上下8bit入れ替え)
2. アドレス32から始まる4byte(32bit)には、対応する32コのLEDがついている(エンディアン入れ替え)
3. アドレス0から始めるリセットボタン

この架空のRISC-Vコンピューターに1+1を計算させるプログラムを上記asm15rを使った組みます。

R10=1 R10+=1 R11=32 [R11]=R10

メモリアクセスにレジスタR8以降しか使えないので、計算もレジスタR10とR11を使うことにします。


他にも、コンピューターにおいて大事な分岐や、複雑な計算の一時記憶に便利な機構スタックを実現する命令などが定義されています。

計算が終わった後もどんどん次のメモリを命令として実行していってしまうので、GOTO命令を使ってはじめに戻してあげましょう。

R10=1 R10+=1 R11=32 [R11+0]=R10 GOTO -4

この5つ、20byteの命令を命令表を見ながらビット列に置き換える、ハンドアセンブルし、スイッチを設定。(この作業を自動化するソフトがアセンブラやコンパイラ)

001 0 01010 00001 01 :'R10=1 000 0 01010 00001 01 :'R10+=1 001 1 01011 00000 01 :'R11=32 110 000 011 0 0 010 00 :'[R11+0]=R10 101 1 1 11 1 1 1 100 1 0 1 :'GOTO -4

これでLEDの右から2つ目だけが光って、1+1の計算結果が2であることを出してくれるプログラム、完成です!
スイッチをいじって、2+1の計算などにしてみましょう。

この架空のコンピューター、作ってみたくなりますね!
RISC-Vは誰もがオープンに使えるアーキテクチャ、また本ブログもCC BYのオープンデータ、自由に作って遊んだり売ったりして、OKです!

Armは、スマホや、IchigoJam、開発中の日本のスパコン富岳など、世界で最も普及しているCPUアーキテクチャ。Armの一番シンプルなCortex-M0のマシン語と比較してみましょう。

Cortex-M0 Armマシン語表 (asm15)
こちらも16bitが基本、似てますね。RISC-Vの縮小命令は32bitの標準命令と下位2bitで区別できるので混在できるのが特徴です。(RISC-Vの縮小命令には掛け算割り算がないので、32bit命令を使用)

Cortex-M0はレジスタ16本(プログラムカウンター含む)、RISC-Vは32本(内R0は0固定、プログラムカウンターは別)など、細かな違いの理由に思いを馳せるのもまた一興。

IchigoJamで、いますぐ遊べるマシン語の世界もぜひどうぞ!
はじめてのマシン語 - IchigoJamではじめるArmマシン語その1

CPUの世界にもオープンデータ。オープンソースでオープンライセンス(BSDライセンス)なCPUアーキテクチャ「RISC-V(リスク・ファイブ)」の実機が続々登場! 1981年にバークレー大学から発表されたRISC-Iからスタートして5世代目のRISC-V、Google / NVIDIA / NXP / Qualcomm など、数多くの企業をメンバー企業に支えられて躍進前夜。

Amazonが発表した自社開発CPU、Graviton2はArmベースでしたが、次世代がRISC-Vになっても不思議はありません。(現時点でAmazonはまだメンバーではないが、オープンに誰でも使えるため、使用するのにメンバーである必要もない)

そんな RISC-V というオープンなコンピューターの気持ちになってみましょう!
IchigoJamで試した、はじめてマシン語Arm編のように、はじめのいっぽはハンドアセンブル!
Specifications - RISC-V Foundation(RISC-V仕様書 P130より)

Instructionsとは、RISC-Vというコンピューターができること一覧、基本は32bit(4byte)、メモリに書かれた命令にしたがって、どんどん計算していきます。 レジスタは基本32コ、そのうち0番レジスタX0は0固定。実行位置を表すプログラムカウンタ(PC)は別となっています。

ひとまず、1+1をさせてみましょう。
レジスタ同士の足し算は ADD (上の図の一番下)

ADD rd,rs1,rs2

rd = rs1 + rs2 という足し算をします。
RISC-Vは、32コあるレジスタ(X0-X31)の内、x10,x11...を引数や、返り値に使うことになっています(x10,x11は別名a0,a1)

ADD x10,x10,x11

こちらをマシン語表を見て、二進法32桁の数値に変換します。(RISC-Vのマシン語の基本は32bit、16bitの圧縮版もあります)

0000000 01011 01010 000 01010 0110011         rs2 rs1 rd

これを16進数に表すと

-> #00b50533

この4byteをメモリに書き込めばOKです!(RISC-Vは、little endian なので、#33,#05,#b5,#00 の順)

ただ、これだけではプログラムの終わりがわからずどんどんよくわからない計算が進んでしまうので、戻す(RETURN)命令を加えます。 RISC-Vでは、戻り先がレジスタ1番に入っているので、そちらへのジャンプ(GOTO)すればOK。 命令表の中から Brunch の JALR x1 を上記同様に16進数に変換します。(x1は別名ra)

JALR x1 (JALR rd,rs1) 0b00000000000 000001 000 00000 1100111         rs1 rd -> #00008067

できました!

実機で動かしてみたいところですが、今回は誰でも使えるエミュレーターを使います。

WebAssemblyにも対応し、ブラウザ上でRISC-V版Linux(JSLinux)まで動かしてしまうTinyEMUをベースにしたコンパクトなエミュレーターrv32emuを使います。

いろいろ組み込みに便利なようにコア部分を分離するように改造した rv32emu を作りました。
emu-rv32i.h をインクルードし、下記のように使います。

#include "emu-rv32i.h" #include <stdio.h> int main(int argc, char** argv) { uint32_t start = 0; ram_start = 0; uint32_t end = 0xfffffffe; *(uint*)(ram + start + 0) = 0x00b50533; // ADD x10,x10,x11 *(uint*)(ram + start + 4) = 0x00008067; // JALR x1 pc = start; reg[2] = ram_start + RAM_SIZE; // sp - stack pointer reg[1] = end; // ra - return adderss reg[10] = 1; // a0 reg[11] = 1; // a1 while (machine_running) { next_pc = pc + 4; insn = get_insn32(pc); printf("[%08x]=%08x\n", pc, insn); execute_instruction(); pc = next_pc; if (pc == end) break; } printf("x10 a0: %08x\n", reg[10]); return 0; }

結果はこちら

x10 a0: 00000002

見事、x10に 1+1 の結果、2が入っていますね!
reg[10]やreg[11]の初期値を変えたり、他の命令をハンドアセンブルしてみたりと、いろいろ試してみましょう。

MacでRISC-Vの開発環境を入れるなら、Homebrewを使うと楽ですよ!

$ brew tap riscv/riscv $ brew install riscv-tools

links
- はじめてのマシン語 - IchigoJamではじめるArmマシン語その1

Tweet
クリエイティブ・コモンズ・ライセンス
この作品は「Creative Commons — CC BY 4.0」の下に提供されています。
CC BY / @taisukef / アイコン画像 / プロフィール画像 / RSS