初心者にはオススメできないC言語ですが、多くの高専の授業で登場するC言語

#include <stdio.h> int main() { printf("Hello World!\n"); return 0; }

このC言語のプログラムを実行すると

Hello World!

と出ます。これで感激できるのはC言語を作った人と、環境構築に苦労した人くらい。大抵の人は「で?」と思うだけでしょう。 なぜ # で始まる? <stdio.h> はHTMLのタグとは違う? studio のつづり違い?など1行目から分からないことだらけです。

こちら、その謎に本質から迫るため開発した、x64版の超シンプルマシン語標準入出力ライブラリ stdio.asm を Windows 10で動かした様子。Mac/Linux/FreeBSDでも同様に動きます。

今日の記事はC言語を少しやったけど、なんだかスッキリしない人、現代PCの主力CPU、Intel/AMDのx86系x64マシン語を使ったOSの深い話が気になる人向けです。 まずは楽しくプログラミングでゲームを作ってみたい人や、マシン語の基本は、シンプル&コンパクトなパソコン、IchigoJamからスタートするのがオススメです。(IchigoJam入門動画はじめてのマシン語

C言語プログラミング、本当のはじめのいっぽはこちらです。

int main() { return 1 + 1; }

これを test1.c と保存し、コンパイルし、実行し、実行結果を表示すると(for macOS/Linux)

gcc test1.c ./a.out echo $? 2

1 + 1 という人間らしい言葉で、コンピューターに計算させることに成功!

細かく解説すると・・・。 こちら、整数(int)を返すmainと名付けた関数(数学の関数と似たもの)を作り、中身に「1 + 1」を返して(return)と記述したC言語のプログラムです。 mainという名前は特別で、プログラム起動時に最初に呼び出されます。 「1 + 1 」という人にわかりやすい言葉を、コンピューターが分かる言葉、マシン語へ変換してくれるのがC言語のコンパイラというツールです。 gccというコンパイラは、特に何も指定しないと a.out という実行ファイルができます。それを、実行。 プログラムが実行した結果をみるコマンド(echo $?)を使って、コンピューターが計算した結果「2」を得ることができた。というわけです。

ゲームのキャラクターを表示したい、コントローラー入力を使いたい、音を慣らしたい、いろいろ欲が出てきます。 そこで登場するのがライブラリ。コンピューターの仕様を隅から隅まで調べなくても、誰かが作ってくれた便利な部品を使うことで楽できます!

そんなライブラリの代表格が stdio.h(スタンダードI/Oの略) で定義されている標準ライブラリというものです。 基本的な文字の表示(output)と入力(input)のための関数が揃っています。 冒頭に登場した、printfという関数もそのひとつ。

int main() { printf("hello!\n"); return 0; }

上記のようにプログラムを変更してコンパイルすると、printfって何?とエラーになります。

printfを事前に定義する必要があります。stdio.h というファイルに定義があります。#include は指定したファイルを埋め込んでくれます。 ファイル stdio.h はどこにあるのでしょう?コンパイラが含めるために使うディレクトリが決まっていてその中のを使ってというのが、「<」と「>」で囲う意味でした。 その場にある自分で作ったファイルを含めるときは、ダブルクォートで囲みます。 HTML,CSS,BASIC,C,JavaScript,Pythonなど、コンピューター言語毎に、記号の意味は変わります。

文字の表示やサウンド出力などの手順は、Windows/macOS/LinuxなどのOS毎に違います。 C言語のコンパイラはCPUの違いを吸収してマシン語を生成してくれますが、手順の違いを吸収するのはライブラリの役目。

例えば、Macで文字を出力するには、レジスタrsiに出力したい文字列の先頭アドレスを、レジスタrdxに長さを、rdiに1を、raxに0x2000004をそれぞれ設定して、システムコール(syscall)を呼ぶことで、OSが文字を表示してくれます。(stdio_mac64.asm src on GitHub、cmd: nasm -f macho64 hello.asm -DMAC64; ld -lSystem hello.o -o hello )

mov rdi, 1 ; fd = stdout mov rax, 0x2000004 ; syscall write syscall

CentOS, Ubuntu, Debian, OpenSuSE, Arch Lniux, Fedoraなど、多くのLinuxではレジスタraxに設定するシステムコール番号を変えるだけでOKです。(stdio_linux64.asm src on GitHub、cmd: nasm -f elf64 hello.asm -DLINUX64; ld -e _main hello.o -o hello )

mov rdi, 1 ; fd = stdout mov rax, 1 ; syscall write syscall

FreeBSDでは他のLinuxと異なります。macOSはBSD系の子孫、この値に 0x2000000 数を足したものです。(stdio_bsd64.asm src on GitHub、cmd: nasm -f elf64 hello.asm -DBSD64; ld -m elf_x86_64_fbsd -e _main hello.o -o hello )

mov rdi, 1 ; fd = stdout mov rax, 4 ; syscall write syscall

Windowsでは直接システムコールを呼んではいけないことになっていて、dll(ダイナミックリンクライブラリ)を経由しての呼び出しとなります。stdio_stdout の初期化が必要で、レジスタの使い方や、スタックの使い方も違います。(stdio_win64.asm src on GitHub、cmd: nasm.exe -fwin64 hello.asm -DWIN64& link.exe /entry:_main /subsystem:console hello.obj kernel32.lib )

mov r8, rdx ; len mov rdx, rsi ; buffer mov rcx, [rel stdio_stdout] mov r9, stdio_bytesWritten push qword 0 call WriteFile

このようにOSによってそれぞれ使い方は異なりますが、名前と使い方に揃えれば、以後気にしなくてよくなります。stdio.asm で、環境によって使うライブラリを切り替えるようにしておきます。

%ifdef WIN64 %include "stdio_win64.asm" %elifdef MAC64 %include "stdio_mac64.asm" %elifdef LINUX64 %include "stdio_linux64.asm" %elifdef BSD64 %include "stdio_bsd64.asm" %endif

こうして準備をしておき、stdio.asm を include すれば、どのOSでも動くマシン語でプログラム「Hello World!」が、このようにシンプルに書けます。NASMでのファイルへの埋め込みのincludeは#ではなく%。 いろんな環境での動作確認はConoHaのVPSを使うと1環境1時間1円〜と、とても手軽です。

%include "stdio.asm" global _main section .rodata MESSAGE db "Hello World!", 0x0d, 0x0a LEN_MESSAGE equ $-MESSAGE section .text _main: call stdio_init mov rsi, MESSAGE mov rdx, LEN_MESSAGE call stdio_write mov rdi, 0 call stdio_exit ret

開発効率、速度、汎用性の向上や、特殊な用途への特化するためなど、さまざまなプログラミング言語が誕生し、使われたり、廃れたりしていますが、結局動いているのはこれらマシン語です。 基本を抑えておけば、余計な手間をかけることなく、楽しく楽に開発できてますますプログラミングが楽しくなります。

理解を深めるには、実際にやってみるのが一番です!Windows/Mac/Linux/FreeBSD、それぞれ環境に合わせた c-*.sh/bat、c-*-test.sh/bat を使って、実際に動かしてみましょう。(src on GitHub

開発に必要なツールを設定しましょう。Windowsでは「Visual Studio C++ 2019 コミュニティ」、Macでは「Xcode」がそれぞれ無料で提供されています。 アセンブリ言語をマシン語化してくれるツール、アセンブラ「NASM」もオープンソース、無料です。

Visual Studio C++ 2019 コミュニティ、右側赤枠で囲んだ2つ、ビルドツールとSDKがあれば、ひとまず今回の開発は可能です。セットアップ後、コマンドプロンプトで hello-x64asm のディレクトへ移動し init-win.bat を動かし、開発ツールとNASMにPATHを通して、レッツトライ!

stdio.asm は、stdio.h が持つ機能の極一部。自分なりのライブラリや、プログラミング言語づくりにチャレンジするのもおもしろいですよ!

links
- taisukef/hello-x64asm: the first step of x64 64bit assembly programming on Windows/macOS/Linux
- C言語開発者「C言語は初心者にはお勧めできない」 エントリーにオススメBASICは、世界初のクラウド対応言語だった! IoTで起業家甲子園目指す、長岡高専チームメンタリング
- プログラミング言語は何から学ぶべきか? ロボットプログラミングゲームをIchigoJamでプログラミング! C言語の教科書「Springs of C」より
- 高専でなぜC言語を学ぶのか? IchigoJamマシン語生成プログラム c4ij で作る、C言語版かわくだり
- IchigoJamからのステップアップ - IchigoJam BASIC / Python3 / JavaScript / Java / C言語 対照表
- ハンドアセンブルで高速計算! RISC-V、RV32ICエミュレーターのC言語実装
- マシン語対応 IchigoJam web、気軽にハンドアセンブルして遊べます!
- OpenCL/C言語 GPUプログラミングはじめのいっぽ on Mac
- わずか16KB! WebAssemblyで動くミニC言語オフラインコンパイラ&インタプリタ webci0
- 深いプログラミング言語学習に最適! 512行のC言語コンパイラ ci0 を使ってみました
- 地味なC言語がなぜ楽しいのか?

長らく使ってきた macOS Mojave から Catalina へ移行。32bit開発のためにと残して来たがRISC-Vの開発環境が非対応となり決心。 記念に動かなくなってしまう 32bit版bootBASIC を、64bit化。


「bootBASIC_64bit for MacOS」(src on GitHub)
nasmでアセンブル可能。 コンパクトな実装でプログラミング言語づくりの入門にぴったり。 WindowsやLinuxへの移植もチャレンジしたいが、やってみたい方、fork/プルリク、大歓迎! 512byteのブートセクターに収まる「bootBASIC」を開発した、nanochessさん、ありがとう!(Thank you! nanochess-san.)

10 A=1 10 A=1 20 A=A*10 30 PRINT A 40 GOTO 20 >RUN 10 100 1000 10000 100000 1000000 10000000 100000000 1000000000 10000000000 100000000000 1000000000000 10000000000000 100000000000000 1000000000000000 10000000000000000 100000000000000000 1000000000000000000 -8446744073709551616

16bitのAXレジスタは、32bitでEAX、64bitでRAXへと段階的に進化を遂げたx86マシン語。今回変数も64bit化。100京まで表示!

; Compare statement (rsi, rdi, length:rcx) ; case sensitive check (original) ; rep cmpsb ; jne statement_not_match ; Equal? No, jump ; case insensitive check statement_check: mov al, [rdi] and al, 0xdf mov ah, [rsi] and ah, 0xdf cmp al, ah jne statement_not_match inc rdi inc rsi dec rcx jnz statement_check statement_check_end:

やっぱり、BASICなので、大文字表記にも対応したい。rep cmpsb という、ループ付きマシン語命令を大文字小文字を区別しないループに変更。

慣れない言語であってもネット時代、探せばすぐに解決する良い時代。x86マシン語に関して「Jun's Homepage」が参考になりました。 32bitのEAXで代入するとRAXの上位32bitは0クリアされるとか、MULやDIVが暗黙的にRDXが使われるとか、ハマりやすいポイントが分かります。

このサイト作者によるコンパクトなプログラミング言語「Return of Very Tiny Language」を発見!

IchigoJam BASICの元になったTinyBASICより、更にコンパクトなプログラミング言語VTLの64bit版!ファイルや画像も扱えるように大幅パワーアップ!(VTL系言語の歴史

Linux版をmacOSで動くように少しだけ書き換えて、コンソールでシンプルのVTLを体験することができました。
taisukef/rvtl-amd64: RVTL : Tiny BASIC in x86_64 assembly language for macOS (開発途中)

移植で困ったのは、アセンブル時にでるエラー「error: 32-bit absolute addressing is not supported in 64-bit mode」。アドレスに @GOTPCREL(%rip) をつけて回避とありましたが、Intel表記では使えず、ひとまず使ってなさそうなr15レジスタを介する形に修正。

macOS Catalina 化もトラブルなく終了。Windows NT 3.51 (1996年)から続いた32bit開発環境に分かれを告げました(クロス開発は除く)。 当時の開発環境は、Intel Pentium 90MHz、RAM16MB、HDD1GB、VC++。何より技術資料へのアクセス手段が紙頼み。良い時代になりました。

かわいく思えるようになったx86マシン語、深みにハマって行き着いた「アセンブラ短歌
57577の5行、31byteで動くマシン語で、2byte以上の命令が行をまたいではいけないというストイックなルールが良い!

一句詠んでみました。

68 57 61 6B 61 31 C0 68 6D 62 6C 79 68 41 73 73 65 89 E1 6A 0C 51 6A 01 50 B0 04 CD 80 EB E1

タイトル「WakaAssembly...」 (x86/macOS Mojabe 32bit)

コンピューターの凄さを一番すぐに感じられるのは、繰り返し。 人間では不可能な速さで正確無比に繰り返される様を見せつけ、それが自分の指示によるものだと体感したものを虜にしてしまう。 (参考、IchigoJam BASIC のプログラム、RUN on IchigoJam web by WebAssembly

10 PRINT "WakaAssembly"; 20 GOTO 10

アセンブラ和歌アセンブラ和歌と繰り返しから誕生した、WakaAssembly、WebAssemblyっぽくて輸出しやすいかも。

CatalinaにOSアップデートで動作しなくなってしまう、macOS最後の32bitアプリの記念に。

x86マシン語(アセンブリ言語)で書かれたソースはこちら

00000000 <_main>: 0: 68 57 61 6b 61 push 0x796c626d 5: 31 c0 xor eax, eax ; eax = 0 7: 68 6d 62 6c 79 push 0x65737341 c: 68 41 73 73 65 push 0x616b6157 11: 89 e1 mov ecx, esp 13: 6a 0c push 12 ; length 15: 51 push ecx ; buffer 16: 6a 01 push 1 ; stdout 18: 50 push eax ; dummy 19: b0 04 mov al, 4 ; sys_write 1b: cd 80 int 0x80 ; syscall 1d: eb e1 jmp _main

pushで一度に4文字分、直接詰めるのがさすがCISC!jmp _mainで先頭へジャンプし、繰り返すたびにスタックを積み続けるので Segmentation faul を出してちゃんと止まり、現代OSのありがたみを感じる。(bits 32 として、32bitモードでのアセンブルが必要)

行儀よく、スタック位置を合わせ ret で 0 を返して終わるバージョン main-1shot.s、31byteのバイナリを和歌表示するために作ったミニプログラム wakaout.c もあります。
詳しくは「x86asm/wakaasm src on GitHub」で!

links
- アセンブラ短歌 - 坂井弘亮 / slideshare
- 「オープンソース」を使ってみよう (第33回 アセンブラ短歌)
- 31バイトでつくるアセンブラプログラミング ~アセンブラ短歌の世界~ Kindle版
- 「57577」の機械語を詠む「アセンブラ短歌本」まとめ - Togetter
- 根っこは同じ! Z80経験者によるx86マシン語はじめ、512byte bootBASIC をmacOS用に移植

マシン語秘伝の書を片手にx86マシン語入門が楽しい。Z80と8086のレジスタ対応表、根っこが同じと分かれば怖くない。 どちらの設計にも深く関わる、嶋正利さん

512byteのブートセクターに収まりブートする、x86マシン語で書かれた「bootBASIC」を発見! 世界最小のチェスプログラムの作者でもある、nanochessさんによる著作「Programming Boot Sector Games (PDF ver.)」を片手に、x86の入門として、16bitのリアルモード向けのbootBASICをmacOSの32bitアプリへと移植。


パスカルの三角形が動いた!


bootBASICは、使える命令も最小限、マルチステートメントもなしのシンプルなBASIC。固定メモリを使っていた元のコードを .data セクションに移して、si/diを32bit化したesi/ediに変更、BIOSの代わりに syscall を使う形にしてできあがり。 アセンブリ言語を使った開発は、アセンブラ nasm によって瞬時にバイナリ化され、即動作。スピード感が気持ちいい。


「Pascal's Triangle」 RUN on IchigoJam web
bootBASICの構文は、IchigoJam BASICにも使えるので、サンプルのプログラムそのままで動いた、パスカルの三角形!


bootBASIC_32bit src on GitHub
現在、Mac用のみ。


Programming Boot Sector Games (PDF ver.)」
1KBの半分、512byteにコンパクトに収めるためのマシン語の基本や、テクニックなど丁寧に解説された本がステキでした!

32bit化でEが付き、64bit化してRが付きと拡張されはしてますが、8bitの名機Z80(1976年)や、16bitアーキテクチャーの王様8086(1978年)の面影、しっかり残ってます。

Z80マシン語でブイブイいわせた人、最近の重量級開発環境が苦手な人、言語の流行り廃りに疲れた人、アセンブラによる超軽量開発で、マシン語でしか到達できない世界を切り開くとかどうでしょう?

links
- 41年経った今でも使える、いにしえのx86マシン語AAAをMacで動かす
- x86 - Wikipedia
- x64 アセンブリーの概要 | iSUS

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

今使ってるMacBook ProのCPUは、Intel Core i5 2.7GHz コア数:2、64bitの汎用レジスタが16個ある64bit CPU、Intel 64というマシン語が使える。

Macのコンソールに定番の「Hello World!」と表示させるマシン語のプログラムは、このようなアセンブリ言語を使って作る。

SECTION .data MESSAGE db `Hello World!\n` MESSAGE_LEN equ $-MESSAGE SECTION .text global _main _main: mov rax, 0x2000004 ; syscall 4: write mov rdi, 1 ; fd = stdout mov rsi, MESSAGE ; buffer mov rdx, MESSAGE_LEN ; size syscall mov rax, 0x2000001 ; syscall 1: exit mov rdi, 0 ; retcode syscall

アセンブリ言語をアセンブラというソフトを使ってマシン語を生成することをアセンブルと呼ぶ。(2020.1.12 -lSystem を追記)

nasm -f macho64 hello.s ld -lSystem -o hello hello.o ./hello

アセンブラは、NASMというオープンソースなソフトを使う。
Macに入っているバージョンは 0.98.40 と古くIntel 64に非対応だったので、最新版 2.12.02 をダウンロード。

brew install nasm

画面の表示は、raxなどレジスタに画面表示用のコマンド番号や、データのメモリの位置、文字数をセットしてシステムコール(syscall)というOS側に処理を依頼することで実現。プログラムの終了もシステムコールを使ってOSに伝える。(64bitモードのときは0x2000000を加えるみたい)

CPUは高速に2進数のbitをあれこれいじる機械。いじり方によって足し算になったり掛け算になったり、比較したり、次に実行するプログラムを変えたりする。Armマシン語入門で、机と表現したレジスタと、本棚をイメージすると近いRAM(メモリ)を使う設計自体は、世界初の商用マイクロプロセッサ Intel 4004(開発は、日本人、嶋正利さん!)から全く変わっていない。

Intel 4004は、45命令、レジスタは4bit、計算用のAレジスタとR0-R15の16レジスタ
Intel 8008は、68命令、レジスタは8bit、A,B,C,D,E,H,LとZ80とほぼおなじ構成
Intel 8086では、レジスタが16bitになり、AX,BX,CX,DX,SP,BP,SI,DIという構成(SI/DIはセグメント)

Intel 64はその直系で、レジスタが64bit、互換性を持ちつつ拡張された RAX,RBX,RCX,RDX,RSP,RBP,RSI,RDI にR8からR15の8レジスタが追加されている。SIMD命令などで命令数は数百(〜千オーバーかも?)と膨大だが、レジスタでいじってメモリなどに書くという基本は変わらない。

IchigoJamで使っているCortex-M0は、55命令、レジスタは32bit、R0-R15という4004に似たシンプルな設計。最新の64bit版Arm、Arm64もその延長線上にある。

エスケープシーケンスとシステムコールを使うとコンソールの画面で8色の色が使える。


座標をエスケープシーケンスで渡すためにはレジスタを10進数に変換する必要があるので、一度マシン語でつくってみると楽しい。楽しみを奪うといけないので、ソースは公開はまた後日。

PCでブートするIchigoJam PCをベースに、64bitをフルに使う特殊OSを作ってみるのもありかもしれない。

参考リンク
- Intel® 64 and IA-32 Architectures Software Developer Manuals (大元、Intelの資料)
- x64 アセンブリーの概要 | iSUS
- Tips IA32(x86)命令一覧 (32bit版だかイメージはつかめる)
- よく使うASM命令ベスト100位に説明つけてみた - おなかすいたWiki!

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