[kernel]ページング

Pagingとは記憶装置をページと呼ばれる小さな単位に分割して割り当てを行うアルゴリズム群であり、仮想記憶のベースとなる設計
論理メモリから物理メモリ空間への対応づけはページテーブルという構造体で実現する

kmalloc(), kfree()
カーネルが連続した物理メモリ領域を確保するために使用。物理メモリ上に連続した領域を確保することで、空間的局所性が得られ、TLBを最大限活用できるため高速
malloc(), vfree()

– 実メモリ管理
Buddyシステムは、隣り合った空き領域を結合し、より大きな空き領域を作り出そうとする
Slabアロケータは、フラグメントの発生を最小限に抑えることと、メモリキャッシュ利用効率を考慮したメモリ管理方式

– 仮想記憶
仮想空間の機能によって、物理的に分散したページを集めて、連続した仮想空間に割り付ける

尋常ではないな、勉強すればするほど、どんどん自信を失ってくわ。。。なんだこれは。。

[kernel]ネットワークドライバ

Linuxのデバイスには
– キャラクタデバイス: バイト単位のデータ通信
– ブロックデバイス: ブロック単位のデータ通信
– ネットワークデバイス: ブロック単位のデータ通信
L /dev以下にはマウントされない
L システムコールのインターフェースが異なる
L デバイスからも非同期的にカーネルアクセスが発生する

ネットワークドライバに最低限必要な機能は、デバイスの取得及び各種設定、パケット送信、パケット受信

$ dmesg | grep eth0
[ 1.390857] e1000 0000:00:03.0 eth0: (PCI:33MHz:32-bit) 02:43:0e:00:05:fc
[ 1.393083] e1000 0000:00:03.0 eth0: Intel(R) PRO/1000 Network Connection
[ 3.305024] e1000 0000:00:03.0 enp0s3: renamed from eth0

ん、eth0にe1000が当たってる?
$ modinfo e1000
filename: /lib/modules/4.15.0-101-generic/kernel/drivers/net/ethernet/intel/e1000/e1000.ko
version: 7.3.21-k8-NAPI
license: GPL
description: Intel(R) PRO/1000 Network Driver
author: Intel Corporation, srcversion: 10A45E43BF63AE653A22C94

デバイスファイルは/devにある
fd0 フロッピードライブ
fd1 フロッピードライブ
sda ハードディスク
sdb ハードディスク
sda1 最初のハードディスクの最初のパーティション
sdb7 2番目のハードディスクの7番目のパーティション
sr0 CD-ROM
sr1 CD-ROM
ttySO シリアルポート0
ttyS1 シリアルポート1
psaux PS/2 マウスデバイス
gpmdata 擬似デバイス
cdrom
mouse
null 書き込まれたものを全て消す
zero 無限に0を読み出せる

[kernel]システムコール

カーネルはハードウェアの提供する機能を全て利用できるが、それ以外のプログラムはハードウェアの機能の中で利用できないものがある。非特権モードで動作しているプログラムがカーネルに依頼する方法がシステムコール

e.g.
ネットワークを利用した通信、ファイル入出力、新しいプロセス生成、プロセス間通信、コンテナの生成など
fopen(), fclose(), fread(), fwrite()

システムコールの目的
– ハードウェアを操作するシンプルなインターフェイスの提供
– アプリケーションが安全かつセキュアにOSのリソースを利用できる

### アプリケーションがシステムコールを呼び出す仕組み
– 実行するシステムコールを指定する番号をレジスタにセット
– システムコールに引き渡す引数をレジスタにセット
– システムコールを発動するインストラクションを実行

.intel_syntax noprefix
.global main

main:
	push rbp
	mov rbp, rsp
	push 0xA216948 // hi
	mov rax, 1
	mov rdi, 1
	mov rsi, rsp
	mov rdx, 4
	syscall
	mov rsp, rbp
	pop rbp
	ret

$ gcc -o hi hi.s
$ ./hi
Hi!

x86-64
1. raxにsystem call numberをセット
-> 0 read, 1 write, 2 open, 57 fork
2. システムコールに渡したい引数をレジスタにセット
-> rdi, rsi, rdx, r10, r9, r8に引数を格納していく
3. syscallのインストラクションを呼び出す

[kernel]ページテーブル

page table
– OSの中の仮想記憶システムで仮想アドレスと物理アドレスを対応づけるために使われる
– 仮想アドレスはそれにアクセスするプロセスによって実行されるプログラムによって使われ、実際には物理アドレスはRAMサブシステム等のハードウェアによって使われる
– それぞれのプロセスのメモリは物理メモリの色々な領域に散らばっているか、HD等のストレージに移されている

アドレス空間は4Kiバイトに分割され、この塊をページという、物理メモリはフレームと呼ぶ
仮想記憶システムは、仮想アドレスを物理アドレスに変換することで、ページとフレームにマッピングする
TLBはページとフレームのマッピングを格納し、全体の性能を向上させる

CPU <-> MMU(page table) <-> Memory
※MMUはMemory Management Unit
仮想アドレスのうち、上位はMMUに変換し、下位はそのままメモリに送られる

$ objdump -f main

main: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x00000000000005a0

この、start address 0x00000000000005a0が仮想アドレス

### 物理メモリの構造
カーネルテキスト(カーネルの実行可能なコード領域)、カーネルデータ(カーネルが使用する変数領域)、ユーザページ

### 仮想記憶システム
アプリケーションがアクセスするアドレスは、 直接物理メモリのアドレスへ変換されずに、アドレス変換表を利用して、 間接的に物理メモリにアクセスする

静的領域とは、プログラム開始時にメモリに確保され、プログラムが終わるまで解放されないメモリ領域
局所領域とは、関数開始時にメモリに確保され、その関数の終了時に解放されるメモリ領域
ヒープ領域とは、上記の二つと異なり、確保するタイミングも解放されるタイミングも任意

[C言語]メモリ管理

メモリ確保関数
malloc: 指定されたメモリを確保する、確保できない場合はNULLを返す
calloc: 指定されたサイズのメモリブロックを確保、確保した領域を0クリア
realloc: 確保済みのメモリを拡張

メモリ解放関数
free: malloc, calloc, reallocで確保した領域を解放する

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

int main(void){

	// 100byteメモリ確保
	char *p = malloc(100);
	if(p == NULL){
	}

	// 100 * sizeof(int)分のメモリを確保し、中身を0でクリアする
	// p = malloc(100 * sizeof(int)); memset(p, 0, 100*sizeof(int));
	int *p2 = calloc(100, sizeof(int));
	if(p2 == NULL){

	}

	// 100byte確保しているメモリを200byteに拡張
	char *p3 =realloc(p, 200);
	if(p3 == NULL){

	} else {
		p = p3;
	}

	free(p2);
	free(p);


	return 0;
}

[コンパイラ]スタックマシン

スタックマシンでは「スタックにプッシュする」と「スタックからポップする」という2つの操作が基本操作

2*3+4*5
PUSH 2
PUSH 3
MUL

PUSH 4
PUSH 5
MUL

ADD

void gen(Node *node){
	if(node->kind == ND_NUM){
		printf("	push %d\n", node->val);
		return;
	}

	gen(node->lhs);
	gen(node->rhs);

	printf("	pop rdi\n");
	printf("	pop rax\n");

	switch(node->kind){
		case ND_ADD:
			printf("	add rax, rdi\n");
			break;
		case ND_SUB:
			printf("	sub rax, rdi\n");
			break;
		case ND_MUL:
			printf("	imul rax, rdi\n");
			break;
		case ND_DIV:
			printf("	cqo\n");
			printf("	idiv rdi\n");
			break;
	}
	printf("	push rax\n");
}
int main(int argc, char **argv){
	if(argc != 2){
		fprintf(stderr, "引数の個数が正しくありません\n");
		return 1;
	}

	user_input = argv[1];
	token = tokenize(user_input);
	Node *node = expr();

	printf(".intel_syntax noprefix\n");
	printf(".global main\n");
	printf("main:\n");
	
	gen(node);

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

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

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

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

単純な生成規則
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