[コンパイラ]文法記述方法と再帰下降構文解析

*, /, ()を言語に追加するには演算子の優位順位を決めなければならない

– パーサの実装
入力はフラットなトークンの列で出力は入れ子構造を表す木にする

単純な生成規則
expr = num(“+” num | “-” num)*
mul = num(“*” num | “/” num)*
具象構文木(concrete syntax tree)

再帰を含む生成規則
expr = mul(“+” mul | “-” mul)*
mul = primary(“*” primary | “/” primary)
primary = num | “(” epr “)”

再帰下降構文解析
expr = mul(“+” mul | “-” mul)*
mul = primary(“*” primary | “/” primary)*
primary = num | “(” epr “)”

typedef enum {
	ND_ADD,
	ND_SUB,
	ND_MUL,
	ND_DIV
	ND_NUM,
} NodeKind;

typedef struct Node Node;

struct Node {
	NodeKind kind;
	Node *lhs;
	Node *rhs;
	int val;
};

Node *new_node(NodeKind kind, Node *lhs, Node *rhs){
	Node *node = calloc(1, sizeof(Node));
	node->kind = kind;
	node->lhs = lhs;
	node->rhs = rhs;
	return node;
}

Node *new_node_num(int val) {
	Node *node = calloc(1, sizeof(Node));
	node->kind = ND_NUM;
	node->val = val;
	return node;
}

Node *expr(){
	Node *node = mul();

	for(;;){
		if(consume('+'))
			node = new_node(ND_ADD, node, mul());
		else if(consume('-'))
			node = new_node(ND_SUB, node, mul());
		else
			return node;
	}
}

Node *mul(){
	Node *node = primary();

	for(;;){
		if(consume('*'))
			node = new_node(ND_MUL, node, mul());
		else if(consume('/'))
			node = new_node(ND_DIV, node, mul());
		else
			return node;
	}
}

Node *primary(){
	if(consume('(')){
		Node *node = expr();
		expect(')');
		return node;
	}

	return new_node_num(expect_number());
}

[コンパイラ]トークナイザー

文字列をトークン列に分割することをトークナイズするという

#include <ctype.h> // 文字の種類の判定や文字の変換
#include <stdarg.h> // 可変長引数
#include <stdbool.h> // bool, true, false
#include <stdio.h>
#include <stdlib.h> // strtol
#include <string.h>

typedef enum {
	TK_RESERVED, // 記号
	TK_NUM, // 整数トークン
	TK_EOF // 入力の終わりを表すトークン
} TokenKind;

typedef struct Token Token;

struck Token {
	TokenKind kind; // トークンの型
	Token *next; // 次の入力トークン
	int val; // kindがTK_NUMの場合、その数値
	char *str; // トークン文字列
};

// 現在着目しているトークン
Token *token;

// エラーを報告する為の関数
// printfと同じ引数
void error(char *fmt, ...){
	va_list ap;
	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	fprintf(stderr, "\n");
	exit(1);
}

// 次のトークが期待している記号の時には、トークンを1つ読み進めて真を返す、それ以外の場合には偽を返す
bool consume(char op){
	if(token->kind != TK_RESERVED || token->str[0] != op)
		return false;
	token = token->next;
	return true;
}

// 次のトークが期待している記号の時には、トークンを1つ読み進めて真を返す、それ以外の場合にはエラーを返す
void expect(char op){
	if(token->kind != TK_RESERVED || token->str[0] != op)
		error("'%c'ではありません", op);
	token = token->next;
}

// 次のトークが数字の場合、トークンを1つ読み進めて真を返す、それ以外の場合にはエラーを返す
int expect_number(){
	if(token->kind != TK_NUM)
		error("数ではありません");
	int val = token->val;
	token = token->ext;
	return val;
}

bool at_eof(){
	return token->kind == TK_EOF;
}

// 新しいトークンを作成してcurに繋げる
Token *new_token(TokenKind, Token *cur, char *str){
	Token *tok = calloc(1, sizeof(Token));
	tok->kind = kind;
	tok->str = str;
	cur->next = tok;
	return tok;
}

// 入力文字列pをトークナイズしてそれを返す
Token *tokenize(char *p){
	Token head;
	head.next = NULL;
	Token *cur = &head;

	while(*p){
		if(isspace(*p)){
			p++;
			continue;
		}

		if(*p == '+' || *p == '-'){
			cur = new_token(TK_RESERVED, cur, p++);
			continue;
		}

		if(isdigit(*p)){
			cur = new_token(TK_NUM, cur, p);
			cur->val = strtol(p, &p, 10);
			continue;
		}

		error("トークナイズできません");
	}

	new_token(TK_EOF, cur, p);
	return head.next;
}


int main(int argc, char **argv){
	if(argc != 2){
		fprintf(stderr, "引数の個数が正しくありません\n");
		return 1;
	}

	token = tokenize(argv[1]);

	printf(".intel_syntax noprefix\n");
	printf(".global main\n");
	printf("main:\n");
	printf("	mov rax, %ld\n", expect_number()); // ldはlong d, strtolは文字列をlongに変換

	while(!at_eof()){
		if(consume('+')){
			printf("	add rax, %d\n", expect_number());
		}

		expect('-');
		printf("	sub rax, %d\n", expect_number());
	}

	printf("	ret\n");
	return 0;
}

新しいプログラミング言語を作る イコール コンパイラを作る ってことなのか。
低レイヤは学習コストが高いけど、一生物の知識がつくな。

[コンパイラ]加減算ができるコンパイラ

5+20-4のような式をアセンブラで書く

.intel_syntax noprefix
.global main

main:
	mov rax, 5
	add rax, 20
	sub rax, 4
	ret

$ cc -o tmp tmp.s
$ ./tmp
$ echo $?
21

これをCで書く

#include <stdio.h>
#include <stdlib.h> // strtol

int main(int argc, char **argv){
	if(argc != 2){
		fprintf(stderr, "引数の個数が正しくありません\n");
		return 1;
	}

	char *p = argv[1];

	printf(".intel_syntax noprefix\n");
	printf(".global main\n");
	printf("main:\n");
	printf("	mov rax, %ld\n", strtol(p, &p, 10)); // ldはlong d, strtolは文字列をlongに変換

	while(*p){
		if(*p == '+'){
			p++;
			printf("	add rax, %d\n", strtol(p, &p, 10));
			continue;
		}

		if(*p == '-'){
			p++;
			printf("	sub rax, %d\n", strtol(p, &p, 10));
			continue;
		}

		fprintf(stderr, "予期しない文字です: '%c'\n", *p);
		return 1;
	}

	printf("	ret\n");
	return 0;
}

strtolは数値を読み込んだ後、第2引数のポインタをアップデートして、読み込んだ最後の文字の次の文字を指すように値を更新

$ make
$ ./9cc ‘5+20-4’
.intel_syntax noprefix
.global main
main:
mov rax, 5
add rax, 20
sub rax, 4
ret

[コンパイラ]テストスクリプト

test.sh

#!bin/bash
assert(){
	expected="$1"
	input="$2"

	./9cc "$input" > tmp.s
	gcc -o tmp tmp.s
	./tmp
	actual="$?"

	if [ "$actual" = "$expected" ]; then
		echo "$input => $actual"
	else
		echo "$input => $expected expected, but got $actual"
		exit 1
	fi
}

assert 0 0
assert 42 42

echo OK

$ ls
9cc 9cc.c test.sh tmp tmp.s
$ sudo chmod a+x test.sh
$ sh test.sh
0 => 0
42 => 42
OK

Makefile

CFLAGS=-std=c11 -g -static

9cc: 9cc.c

test: 9cc
	sh test.sh

clean:
	rm -f 9cc *.o *~ tmp*

.PHONY: test clean

[コンパイラ]初めてのコンパイラ作成

GCCの構文解析は、再帰下降構文解析法(recursive descent parsing)を使っている

下記のシンプルなアセンブリもコンパイラと言える。

.intel_syntax noprefix
.globl main

main:
        mov rax, 42
        ret

### Intel記法とAT&T記法

mov rbp, rsp // intel
mov %rsp, %rbp // AT&T ...結果レジスタが第二引数にくる

mov rax, 8 // intel
mov $8, %rax // AT&T ...数字には$をつける

mov [rbp + rcx * 4 - 8], rax // intel
mov %rax, -8(rbp, rcx, 4) // AT&T 

### コンパイラの作成

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv){
	if(argc != 2){
		fprintf(stderr, "引数の個数が正しくありません\n");
		return 1;
	}

	printf(".intel_syntax noprefix\n");
	printf(".global main\n");
	printf("main:\n");
	printf("	mov rax, %d\n", atoi(argv[1]));
	printf("	ret\n");
	return 0;
}
$ gcc -o 9cc 9cc.c
$ ./9cc 345 > tmp.s
$  cat tmp.s
.intel_syntax noprefix
.global main
main:
	mov rax, 345
	ret

$ gcc -o tmp tmp.s
$ ./tmp
$ echo $?
89

あれ? 345が89になるな。何故だ? あ、0〜255の範囲でないと行けないからですな。

[コンパイラ]アセンブリ

### 関数のアセンブリ変換
呼び出した関数が終了した後に、元々実行していたアドレス(リターンアドレス)に戻ってくる必要がある
リターンアドレスはメモリ上のスタックに保存される
スタックは、スタックの一番上のアドレスを保持する1つの変数のみを使って実装できる
スタックトップを保持している記憶領域をスタックポインタという

c

#include <stdio.h>

int plus(int x, int y){
	return x + y;
}

int main(){
	return plus(3, 4);
}

アセンブリ

.intel_syntax noprefix
.globl plus, main

plus:
		add rsi, rdi
		mov rax, rsi
		ret

main:
        mov rdi, 3
        mov rsi, 4
        call plus
        ret

第一引数はRDIレジスタ、第二引数はRSIレジスタに入れる約束になっている
callは関数を呼び出す。具体的にはアドレスをスタックにプッシュし、ジャンプ
addはrsiレジスタに上書きされて保存される
返り値はRAXに入れることになっている
retでスタックからアドレスを1つポップ

CPUはメモリを読み書きすることでプログラムの実行を進めていく

[コンパイラ]CPUの命令とアセンブリ

コンパイラは、構文解析、中間パス、コード生成というステージがある
コンパイラが動作するマシンのことを「ホスト」、コンパイラが出力したコードが動作するマシンのことを「ターゲット」という
ホストとターゲットが異なるコンパイラをクロスコンパイラという

### CPU
現在実行中の命令アドレスをCPU内部に保持していて、そのアドレスから命令を読み出して、そこに書かれていることを行い、そして次の命令を読み出して実行する
現在実行中のアドレスのことをプログラムカウンタ(PC)やインストラクションポインタ(IP)という
cpuが実行するプログラム形式そのもののことを機械語(machine code)という
CPUの分岐命令(branch instruction)を使うと、次の命令以外の任意のアドレスに設定することができる(ジャンプ、分岐) eg.if文やループ文など
プログラムカウンタ以外にも少数のデータ保存領域を持っており、レジスタと呼ぶ
2つのレジスタの値を使って何らかの演算を行い、結果をレジスタに書き戻すというフォーマットになっている
命令を命令セットアーキテクチャ(instruction set architecture)という
x86-64命令セットはAMD64、Intel64, x64などと呼ばれることがある
-> awsでインスタンスを作ろうとするとき “64-bit x86″と表示されるが、このx86は命令セットのこと

### アセンブラ
アセンブリは機械語にほぼそのまま1対1で対応する言語
$ objdump -d -M intel /bin/ls

/bin/ls: file format elf64-x86-64

Disassembly of section .init:

0000000000003758 <_init@@Base>:
3758: 48 83 ec 08 sub rsp,0x8
375c: 48 8b 05 7d c8 21 00 mov rax,QWORD PTR [rip+0x21c87d] # 21ffe0 <__gmon_start__>
3763: 48 85 c0 test rax,rax
3766: 74 02 je 376a <_init@@Base+0x12>
3768: ff d0 call rax
376a: 48 83 c4 08 add rsp,0x8
376e: c3 ret

——–
3758: 48 83 ec 08 sub rsp,0x8
3758はメモリのアドレス
48 83 ec 08は命令の機械語
sub rsp,0x8はアッセンブリ rspから8を引く

### Cとアセンブラ

#include <stdio.h>

int main(int argc, char** argv[]){

	return 42; // 終了コード

}

$ gcc -o test test.c
$ ./test
$ echo $?  // 終了コードは$?にセットされる
42

### アセンブリファイルの作成
アセンブリファイルの拡張子は.s

.intel_syntax noprefix
.globl main
main:
	mov rax, 42
	ret

プログラミング勉強の良いところは、後からどんどん繋がってくることだな

FPGA

FPGAとは?
-> Field Programmable Gate Arrayの略
-> 現場で書き換え可能な論理回路の多数配列:ハードウェア言語で修正が出来るデバイス

ハードウェア言語とは一般的に半導体の回路記述をする際に用いる言語
論理回路とはデジタル信号を扱う回路のこと
論理合成と配置配線はハードウェア言語で記述された回路をFPGAに書き込む為のデータに変換すること

CPLDは汎用ロジックIC数百個~数千個分の回路を内部で構成できる
LSIを超える規模の回路を簡単に構成できる
PLGの一種

【組み込み系の開発工程】
要求分析→要件定義→基本設計・詳細設計→RTL設計(Verilog、VHDLなどを用いてコーディング) →論理合成→動作シミュレーション→配置配線

ビッグデータのデータ処理やディープラーニング向けの並列計算等にFPGAやASICが使われることもある

[CPU]命令セット

– 命令の集まり
– コンピュータで使われる命令の表現形式と各命令の動作を定めたもの
命令 = 操作オペランド + 対象
L ソースオペランド
L デスティネーションオペランド
オペランドとなるものはデータレジスタ、メモリ語、プログラムカウンタ、その他レジスタ

### 命令の表現形式
(1)R型: op(5:操作コード) rs(5:オペランドレジスタ) rt(5) rd(5) aux(11:実行細則)
(2)I型: op(6) rs(5) rt(5) imm/dpl(16:immediate displacement)
(3)A型: op(6) addr(26:メモリアドレス)
命令語が32ビット、命令セットの大きさが64、レジスタ数が32

アセンブリ言語表現
R型:add r2 r3 r1 0
I型:subi r2 r1 14
A型:j 1048581

算術論理演算命令(R型(整数)、I型、 R型(浮動小数点))
加算:add, addi, fadd
減算:sub, subi, fsub
乗算:mul, muli, fmul
除算:div, divi, fdiv
除余:rem, remi
絶対値:abs, , fabs
算術左シフト: sla, ,
算術右シフト: sra, ,
論理積:and, andi
論理和:or, ori
否定:not,
NOR:nor, nori
NAND:nand, nandi
排他的論理和:xor, xori
EQUIV:eq, eqi
論理左シフト:sll
論理右シフト:srl

命令の動作はオペランドがデコーダでALUに行き、レジスタのアドレスを取得して計算する

シリコンチップで集積回路を作るには、シリコン上の構造物の配置を図面に落とす必要がある
基本素子を作り、それを配置していく

ICチップを作るには、まず原材料としてシリコンウェハー(Si)が必要
ピラニア溶液(H2SO4:H2O2)、RCA1(H2O:NH3:H2O2)、RCA2(H2O:HCL:H2O2)で洗浄してHF液につけて自然酸化膜を作る

なるほど、CPUの設計って回路設計だけでなく、物理、化学、光学、電気学などかなり幅広い知識を応用してんだな。

[C言語]カーネル

Unix系では新しいプロセスはfork()により生成されexec()系の関数により新たなプログラムに書き換わる
プログラムを実行する為のライブラリ関数としてexeclp()やexecvp()などがある
execveシステムコールが発行されるとカーネルの仕事となる
– 実行ファイルを読み込み、仮想メモリ上にマッピングする
– argc/argv[]、BSSの初期化、環境変数の引き渡しなどを行う
– 実行ファイル上のエントリポイントから実行を開始する

※カーネルを読み込むには5年程度の技術と時間がかかる
カーネルの大部分はC言語で書かれているが、CPU固有の処理部分はアセンブラ言語で書かれている

## カーネルのディレクトリ
Documentation ドキュメント
arch アーキテクチャ依存
block ブロック入出力層
crypto 暗号化
drivers デバイス・ドライバ
firmware ファームウェア
fs VFS(Virtual File System)とファイル・システム
include カーネル用のヘッダ・ファイル
init 起動と初期化(initialization)
ipc プロセス間通信(interprocess communication)
kernel カーネルの中心部分
lib ヘルパ
mm メモリ管理(memory management)
net ネットワーク
samples サンプルコード
scripts カーネルのコンパイルに必要なスクリプト
security Lnux Security Module
sound サウンド・サブシステム
tools カーネル開発用のツール
usr 起動直後にユーザ空間で実行されるプログラム(initramfs)
virt 仮想化インフラ

C言語ではcrt(C RunTime startup)というアセンブリで記述されたプログラムが一番最初に実行される
OSの開発ではこのcrtを環境に合わせて用意する