- 2024-11-09
「低レイヤを知りたい人のためのCコンパイラ作成入門」第十二回勉強会まとめ
学習範囲ステップ5:四則演算のできる言語の作成四則演算のできる言語の作成前章までに作ってきたコンパイラを拡張して、優先順位のカッコを含む四則演算の式を扱えるようにしました。※arm64環境のコードです。int main(int argc, char **argv) { if (argc != 2) { error("引数の個数が正しくありません"); return 1; } //トークナイズしてパースする user_input = argv[1]; token = tokenize(argv[1]); Node *node = expr(); //アセンブリの前半部分を出力 printf(".globl main\n"); printf("main:\n"); //抽象構文木を下りながらコード生成 gen(node); printf(" ret\n"); return 0;}5 + 20 -4 のアセンブリの解説ロード命令 mov : データをある場所(レジスタや即値)からレジスタにコピーすること。 ※即値とはプログラム内に直接書き込まれた具体的な数値やデータのこと。 ldr : メモリに格納されている値をレジスタに読み込むこと。 str : レジスタに入っている値をメモリに書き出すこと。 ※レジスタとは記憶装置のこと [sp, #-16]! : 保存する場所以下はarm64 gccの環境で出力されたアセンブリです。それぞれの命令にコメントを追加しました。.globl mainmain: //レジスタw0に5を入れる mov w0, 5 //スタックに5をプッシュする str w0, [sp, #-16]! //レジスタw0に20を入れる mov w0, 20 //スタックに20をプッシュする str w0, [sp, #-16]! //スタックから20を取り出し、w1に格納する ldr w1, [sp], #16 //スタックから5を取り出し、w0に格納する ldr w0, [sp], #16 //w0とw1を加算し、その結果をw0に格納する。つまり20+5を計算している add w0, w0, w1 //25をスタックにプッシュする str w0, [sp, #-16]! //レジスタw0に4をロードする mov w0, 4 //4をスタックにプッシュする str w0, [sp, #-16]! //スタックから4を取り出し、w1に格納する ldr w1, [sp], #16 //スタックから25を取り出し、w0に格納する ldr w0, [sp], #16 //w0からw1を減算し、その結果をw0に格納する。つまり25-4を行う sub w0, w0, w1 //計算結果21をスタックにプッシュする str w0, [sp, #-16]! ret参考 Linux で Arm64 アセンブリプログラミング (04) ロード命令コラム:9ccにおけるメモリ管理ガーベッジコレクションプログラムで使っていないメモリを解放する機能のことです。サーバーが動いている限りプログラムは実行されるので、使わないメモリは解放されないと永遠に残ってしまいます。プログラムが確保したメモリを占領して解放されずメモリの使える領域が減っていくことをメモリリークといいます。メモリポリシー手元で動かす程度の短命なプログラムにおいては、わざわざ複雑な技術であるガーベッジコレクションは実装しないとった手段をとることもあります。その理由は、手動メモリ管理によって引き起こされる不可解なバグが発生するのを防ぐためです。このようにあえてメモリ管理を行わないメモリポリシーもあります。まとめ今回の実装で優先順位のカッコを含む四則演算の式を扱えるようなりました。またアセンブリを読み解くことで正しく動作しているか確認することができました。テストが失敗しているときは、アセンブリを読むことでデバッグできるという事も学びになりました。次回は"-"から始まる式を扱えるようになります。2項の-は左辺から右辺を引く演算として定義されていますが、単項-にはそもそも左辺がないので、2項-の定義は正しく計算ができません。-から始まる式をどのように拡張して扱えるようにするのか楽しみです。 - 2024-11-01
「低レイヤを知りたい人のためのCコンパイラ作成入門」第十一回勉強会まとめ
学習内容・スタックマシンへのコンパイル・x86-64におけるスタックマシンの実現方法今回やること1.抽象構文木をコンパイルするにはどうするかを考える2.レジスタマシンでどう実現するかを考える1.抽象構文木をコンパイルするにはどうするかを考える例えば下記のような場合“+"├── B └── A1左の部分木をコンパイルする2右の部分木をコンパイルする3スタックの2つの値を、それらを加算した結果で置き換えるコードを出力で達成される。具体的に 2 + 3 * 4のときは“+"├── "*"│ ├── 4│ └── 3└── 21左の部分木をコンパイルしてPUSH 2される2右の部分木をコンパイルしようとして、再帰的に部分木の左側がコンパイルされてPUSH 3される3部分木の右側がコンパイルされてPUSH 4される4再帰元に戻る、MULが出力される5再起元に戻り、ADDが出力されるこのようにすると抽象構文木をアセンブリにおとしていける2.レジスタマシンでどう実現するかを考えるレジスタマシンであるx86-64でどのように実現するか?→スタックの先頭を指すレジスタを用意してスタックポインタとして扱い、仮想的なスタックマシンとする。C言語ではRSPをスタックマシンのトップ(=プリングルスの一番上)にしているpush 1push 2→メモリに値を登録する。X86-64ではスタックは高いアドレスから低いアドレスに向かって積み上げるように設計されているため、例えば0x0FF8 20x1000 1のように積み上がっていく// 左辺と右辺をRAXとRDIにポップして足すpop rdipop raxadd rax, rdi→メモリを直接足す命令はないので、レジスタにロードして、その後加算する// 足した結果をスタックにプッシュpush rax→スタックポインタが指す位置として指定するため戻す前節との差分PUSH 1PUSH 2ADD→ADDのところで実際に計算をするためにロードして、その結果をスタックにプッシュし直すところが違う具体的に 3 + 1 * 2をx86-64形式で書いてみると// 3 をスタックにプッシュpush 3// 1 * 2 を計算して結果をスタックにプッシュpush 1push 2pop rdipop raxmul rax, rdipush rax// スタックトップの 2 つの値を足す// つまり 3 + 1 * 2 を計算するpop rdipop raxadd rax, rdi ; rax = rax + rdi (3 + 2)push rax ; 最終結果 (5) をスタックにプッシュここまでの流れをC言語で表すと下記のようになる// nodeが1 * 2だとするとするvoid gen(Node *node) { if (node->kind == ND_NUM) { printf(" push %d\n", node->val); return; } // 数字だったらpushする gen(node->lhs); // 左部分木を処理。この場合1 ←ここが再帰的 gen(node->rhs); // 右部分木を処理。この場合2 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”); // 結果をプッシュし直す}所感・仮想スタックマシンのロジックを考える→実際にコードにしてみるというながれがおもしろい - 2024-10-26
「低レイヤを知りたい人のためのCコンパイラ作成入門」第十回勉強会まとめ
学習範囲パーサの作成とスタックマシンの概要までパーサの作成確認のため四則演算の生成規則は以下のような形となっています。expr = mul ("+" mul | "-" mul)*mul = primary ("*" primary | "/" primary)*primary = num | "(" expr ")"抽象構文木のノード型です。// 抽象構文木のノードの種類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; // kindがND_NUMの場合のみ使う};前回記述した関数とデータ型を使ってパーサを書いていきます。expr関数expr = mul ("+" mul | "-" mul)* 意味: exprは1つのmulを受け取り、"+"か"-"で連結する式です。このexpr式の生成規則が、そのまま関数呼び出しとループにマップされています。consumeというのは前回定義した関数で、入力トークンの次のトークンがマッチしたときに、入力を1トークン読み進めて真を返します。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; }}mul関数mul = primary ("*" primary | "/" primary) *意味: mulは1つ以上のprimaryを受け取り、"*" や "/" で連結する式です。このmul式の生成規則が、そのまま関数呼び出しとループにマップされています。Node *mul() { Node *node = primary(); for (;;) { if (consume('*')) node = new_node(ND_MUL, node, primary()); else if (consume('/')) node = new_node(ND_DIV, node, primary()); else return node; }}primary関数primary = num | "(" expr ")"意味: 数値や括弧で囲まれた式を表しています。以下のコードはprimary式の生成規則を表しています。Node *primary() { // 次のトークンが"("なら、"(" expr ")"のはず if (consume('(')) { Node *node = expr(); expect(')'); return node; } // そうでなければ数値のはず return new_node_num(expect_number());}呼び出し例例として1+2 * 3という式で考えてみます。ステップ1.expr→mul→primaryというように関数呼び出しが行われて、返り値として1を表す構文木が返される。ステップ2.exprの中のconsume('+')という式が真になるので、+というトークンが消費され、mulが再度呼び出される。残りは2*3となる。ステップ3.2というトークンが読み込まれるが、リターンはせずにmulの中のconsume('*')という式が真になるため、mulは再度primaryを呼び出す。ステップ4.3というトークンを読み込む。結果としてmulからは2*3を表す構文木が返る。ステップ5.リターンした先のexprでは、1を表す構文木と2 * 3を表す構文木が組み合わされて、1+2*3を表す構文木が構築される。スタックマシンスタックとはデータを積み木のように積み上げていくデータ構造のことです。1. 新しくデータを積む場合には一番上に置くこれを "PUSH" という2. データを取り出す場合には一番上のものを一つ取り出すこれを "POP" という3. 要素を2つ取り出して、その結果を元の積み木の一番上に戻すこと。これを "ADD" という2 * 3 + 4 * 5を計算する例です。// 2 * 3を計算PUSH 2PUSH 3MUL※格納1には6が入っている// 4 * 5を計算PUSH 4PUSH 5MUL※格納2には20が入っている// 2 * 3 + 4 * 5を計算ADD※2箇所で格納している、値をそれぞれ取り出して足して新たな格納3に26という値で収納するまとめ生成規則を関数として定義することでより柔軟な計算を行えることができました。これまでは一つの式の解しか保存していませんが、スタックの概念を使うことで複数の解を格納して計算できるようになります。徐々に電卓に近づいていると感じました。 - 2024-10-25
「低レイヤを知りたい人のためのCコンパイラ作成入門」第九回勉強会まとめ
学習内容・再帰を含む生成規則前回との差分前回expr = mul("+" mul | "-" mul) *mul = num("*" num | "-" num) *今回expr = mul ("+" mul | "-" mul)*mul = primary ("*" primary | "/" primary)*primary = num | "(" expr “)” ←newなぜprimaryが必要なのか?→より優先順位が高い()かっこを含む計算をするため出力例まずは()を使わない式をprimaryを使って表現してみる例:2 * 4[expr]└── [mul] ├── [primary] │ └── [num] ── 2 ├── "*" └── [primary] └── [num] ── 41 exprでmulを処理2 mulでprimaryに基づいて2を表す3 "*"をmulに追加4 mulでprimaryに基づいて4を表す例:1 + 2[expr]├── [mul]│ └── [primary]│ └── [num] ── 1├── "+"└── [mul] └── [primary] └── [num] ── 21 exprでmulを処理してprimaryに基づいて1を表す2 exprに"+"を追加3 exprでmulを処理してprimaryに基づいて2を表す例:(1 + 2)[expr]└── [mul] └── [primary] └── "(" ├── [expr] │ ├── [mul] │ │ └── [primary] │ │ └── [num] ── 1 │ ├── "+" │ └── [mul] │ └── [primary] │ └── [num] ── 2 └── ")"1 exprでmulを処理してprimaryに基づいて"("を表す2 かっこ内のexprで再起的に処理をしていく3 primaryに基づいて")"が発見される例:2 + (1 + 3 * 4)[expr]├── [mul]│ └── [primary]│ └── [num] ── 2├── "+"└── [mul] └── [primary] └── "(" ├── [expr] │ ├── [mul] │ │ └── [primary] │ │ └── [num] ── 1 │ ├── "+" │ └── [mul] │ ├── [primary] │ │ └── [num] ── 3 │ ├── "*" │ └── [primary] │ └── [num] ── 4 └── ")"所感定義がしっかり理解できれば解析はやりやすい - 2024-10-12
「低レイヤを知りたい人のためのCコンパイラ作成入門」第八回勉強会まとめ
学習範囲文法の記述方法と再帰下降構文解析生成規則による演算子の優先順位の表現まで四則演算のルールを定義する1+2*3私たち人間はこの式を見たとき、頭の中で「2 * 3を先に計算して6になって、+1をして7になるな」とルールに従って計算をしています。「1+2を先に足して3になって、* 3をして9になるんだな」とはなりません。コンピューターにも同様にルールを教えてあげて計算させる必要があります。木構造による文法構造の表現1+2*3という式を木のように表現してみます。構文木(syntax tree)といいます。そして木の末端から計算していくというルールを設定します。そうすることでシンプルに式の構造を表すことができます。+├── 1└── * ├── 2 └── 3このようにして、コンパイラは入力のトークン列を構文木に変換してから、アセンブリに変換するようします。生成規則による文法の定義世界の言語の基本語順の分布一般的にはS(Subject/主語)・O(Object/目的語)・動詞(Verb/動詞)の3個の並び方によって分類されます。3つのものを重複なしに並べるので、理屈的には6種類(3×2×1)が考えられます。1.SOV2.SVO3.VSO4.VOS5.OVS6.OSVこういったルールに沿って展開していくことで、意味のある文を無数に作り出すことができます。コラム:チョムスキーの生成文法それまで一人一人の個人が白紙の状態から、言語は習得・蓄積されていくものだと考えられていました。しかしチョムスキーは人間は生まれながらにして文法を自由に扱える回路を備えていると提唱しました。これは当時の言語学にとって大きなインパクトを与えました。そしてこの生成文法の背景にある、母国語の種類にかかわらず人間の脳には全てに備わっている統語能力を普遍文法と呼びました。人間は環境によってどんな言語でも習得ができるので、誰でも多言語を取得することができます。そう考えると母国語以外の言語を学ぶ時に少し前向きな気持ちになれそうです。日本語の語順日本語の語順はSOV型(主語・目的語・動詞)が基本です。語順の自由度が高く、日常会話で主語がなくても会話ができます。私たち日本人は無意識レベルで文法の構造を組み立てて、日常生活で意味を持った文を自由に扱えています。コンピュータにも応用上記のような定義はコンピュータとの親和性がとても高いです。プログラミング言語の構文も同様に「生成規則」を使って定義していきます。参考日本語の語順 階層分析と非階層分析チョムスキーからはじめようEBNF記法生成規則の記法として、BNF(Backus-Naur form)があります。それを拡張したEBNF(Extended BNF)というものがあります。ここまでで我々が作ってきたプログラミングの文法をEBNFを使って説明してきます。以下の記号を使って複雑なルールを簡潔に書き下すことができます。A* :Aの0回以上の繰り返しA? :AまたはεA | B " : AまたはB(...) :グループ化単純な生成規則expr = num("+" num | "-" num) *EBNFを使ったこのような生成規則があるとします。exprは、まずnumが1つありその後に0個以上の「+とnum、あるいは-とnum」があることを定義しています。42-30+2のときは以下のような文字列を作り出せます。exper → num "-" num "+" num → "42" "-" "30" "+" "2"木構造で表すことで展開がよりわかりやすくなるかと思います。[expr]├── [num] ── 2 └── "+"└── [num] ── 30 └── "-"└── [num] ── 42演算子の優先順位を生成規則で定義expr = mul("+" mul | "-" mul) *mul = num("*" num | "-" num) *mulというのが乗除算の生成規則です。今回はexprはmulを経由してnumに展開されるルールになります。これはmulが先に計算され、その後加減算が計算されるルールとなっています。以下は1 * 2 +3 * 4 * 5の構文木です。[expr]├── [mul]│ ├── [num] ── 5│ ├── "*"│ ├── [num] ── 4│ ├── "*"│ └── [num] ── 3├── "+"└── [mul] ├── [num] ── 2 ├── "*" └── [num] ── 1たった少しの単純なルールでコンピュータの計算の柔軟性が上がるのはとても不思議です。まとめ第7回まで加減算は行ってきてはいたが、乗除算は計算の優先順位があるためどのようなロジックで計算するのか気になっていました。今回の学習範囲でたった2行のルールを定義することで、計算の幅が拡張されたことに感動しました。 - 2024-10-11
「低レイヤを知りたい人のためのCコンパイラ作成入門」第七回勉強会まとめ
学習内容ステップ4:エラーメッセージを改良何バイト目で発生したかどんなエラーが発生したか解決手順1.文字列の途中を指すポインタを用意する2.エラー関数で、ポインタと入力の先頭を指しているポインタとの差を取る3.マークをつけてエラー分を出力する1. 文字列の途中を指すポインタをトークン内に用意する文字列全体をuser_inputに代入char *user_input;×user_inputは入力内容、*user_inputは最初は入力内容の最初の位置を表すポインタ。◯user_inputは”先頭の位置のみ”をあらわしている- user_input = argv[1];としてるってことは引数全て入れているのに、なんで先頭のアドレスだけでいいのか?→問題ない。”1 + 2"としたときにuser_inputには1のアドレスしか入っていないが、文字の終わりも定義されているので、読み取ることが可能入力文字数分ルーブさせてtokenをつくる下記の部分while (*p) { // 空白文字をスキップ (省略) }ここで*pは文字をあらわしているwhile(*p)しているけど、文字の終わりはどのように判定しているのか?(どこまでが1単語だってどうやったらわかるのか)→C言語では文字列の最後に\0が入るので、そこで判定している例:入力内容”1 + 2”を入れたときにメモリには下記のようにはいる。アドレス: 0x0010 0x0011 0x0012 0x0013 0x0014 0x0015内容 : '1' ' ' '+' ' ' '2' '\0'pは入力内容、pはアドレスを示す。この場合最初のpは1、pはアドレスである0x0010を指す。文字1つ読み取ったらアドレスを++1する。下記の部分 // 空文字はスキップ if (isspace(*p)) { p++; continue; } (中略) // "+" or "-" のとき if (*p == '+' || *p == '-') { cur = new_token(TK_RESERVED, cur, p++); continue; } // 数値の時 if (isdigit(*p)) { cur = new_token(TK_NUM, cur, p); // p++ としてはいけない。なぜなら、strtol関数でpが進むから cur->val = strtol(p, &p, 10); continue; }tokenにポインタを保存する下記の部分Token *new_token(TokenKind kind, Token *cur, char *str) { Token *tok = calloc(1, sizeof(Token)); tok->kind = kind; tok->str = str; // ここで位置情報を保存 cur->next = tok; return tok;}2. エラー関数で、ポインタと入力の先頭を指しているポインタとの差を取る下記の部分int pos = loc - user_input;復習:locは現在位置、user_inputは文字の先頭位置をあらわしている3. マークをつけてエラーを表示させる下記の部分 fprintf(stderr, "%s\n", user_input); // 入力部分を表示 fprintf(stderr, "%*s", pos, ""); // エラーが発生した位置までpos個の空白を出力 fprintf(stderr, "^ "); // マークをつける vfprintf(stderr, fmt, ap); // エラー文を表示させる実行結果test.shにassert 22 " 5 + 21 -+ 4" と記入5 + 21 -+ 4 ^ 数ではありませんtest.shにassert 41 "foo" と記入foo^ トークナイズできません所感ポインタの理解がかなり難しい。user_inputに入力内容を代入すると先頭位置だけを表すこと、*pが入力内容そのものでありpがアドレスであることを理解するのに時間がかかった。 - 2024-09-29
「低レイヤを知りたい人のためのCコンパイラ作成入門」第六回勉強会まとめ
学習範囲ステップ3トークナイザ導入トークン「単語」のことを「トークン(token)」といいます。日本語や英語、算数の式と同じようにプログラミング言語も単語の列から成り立っています。具体例日本語の場合: 私 は ゆうき です 。英語の場合: My name is yuuki .算数の式の場合:5 + 20 - 4 トークナイズするコンピュータが処理できるように単語の集まりからなる文字列を、空白を取り除いた単語ごとに分割することです。列車を例にしたとき分割したトークン列を連結リストというデータ構造で扱うときの具体例です。トークナイズするとは連結している列車をそれぞれ車両ごとに分割することです。1両目、2両目、3両目。。。といった感じです。トークナイズすることのメリット文字列を意味ある塊に分けることもメリットですが、それぞれの塊ごとに型(文字、数値、記号)を定義することでより扱いやすくなります。トークナイザを導入して改良したバージョンのコンパイラトークンに分割する処理のことをトークナイザといいます。以下のコードは識別(トークナイズ)して、その後解釈をして型をつけています。それぞれのコードが何をしているか分かるようにコメントを記述しました。※以下のコードはM1環境の場合です。#include <ctype.h>#include <stdarg.h>#include <stdbool.h>#include <stdio.h>#include <stdlib.h>#include <string.h>// トークンの種類。typedef enum { TK_RESERVED,//記号 TK_NUM,//整数 TK_EOF,//入力の終わり} TokenKind;//これは文字です、数値ですと宣言して、箱作っているようなもの。typedef struct Token Token;// トークン型struct 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 cunsume(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->next; return val;}//今みてるtokenが終わりか。つまり入力が終わっているかを確認する。 bool at_eof() { return token->kind == TK_EOF;}//新しいトークンを作成してcurに繋げる。//例。new_tokenは列車を一個作る関数。Token *new_token(TokenKind kind, 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; } //例。車両ごとに分割する必要があるため、tokenを新しく作る必要がある if (*p == '+' || *p == '-') { cur = new_token(TK_RESERVED, cur, p++); continue; } //例。12 + 35 - 4 は、記号と数字とを分ける。それがなにかを定義する。 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) { error("引数の個数が正しくありません"); return 1; } //トークナイズする //例。20 + 3 + のとき 20は 後ろにある +しか知らない。最後部を知るために辿っていく。 token = tokenize(argv[1]); //アセンブリの前半部分を出力 printf(".globl main\n"); printf("main:\n"); //式の最初は数でなければならないので、それをチェックして //最初のmov命令を出力 printf(" mov w0, %d\n", expect_number()); //'+<数>’あるいは`-,数`というトークンの並びを消費しつつ //アセンブリを出力 while (!at_eof()) { if (cunsume('+')) { printf(" add w0, w0, %d\n", expect_number()); continue; } expect('-'); printf( " sub w0, w0, #%d\n", expect_number()); } printf(" ret\n"); return 0;}まず識別(トークナイズ)し、次に解釈することで読みやすいコードとなります。エラーを起こす .ピリオド(ドット)は識別に対応できていないのでエラーとなります。tokenize 関数では、以下の3種類のトークンしか認識していないためです。空白文字:スキップします。記号:+ または - のみを TK_RESERVED トークンとして認識します。整数値:0 から 9 の数字のみを TK_NUM トークンとして認識します。コラム言語処理を行うときの基本として、現在は文章を単語などのの何らかの単位に区切り(トークナイズ)して、それらをベクトルに落とし込んでモデルで処理をすることが多いです。トークンの種類私はゆうきです。のとき単語単位のトークン私 / は / ゆうき /です / 。文字単位のトークン私 / は / ゆ / う / き / で / す / 。サブワード単位のトークン単語単位からさらに分割したものをサブワードといいます。東京オリンピックなら東京 / ##オリンピック /「##」とついているのは前に何かのサブワードがつく事を表しています。LLMではこの分割された単語に一意のID(数字)を割り当てて、この数字を使ってベクトル計算をし、文字の類似度をコンピュータで学習することができるようになります。LLM(大規模言語モデル)とは膨大なテキストデータと高度なディープラーニング技術を用いて構築された言語モデルです。大規模言語モデルは、人間に近い流暢な会話が可能であり、自然言語を用いたさまざまな処理を高精度で行えることから世界中で注目を集めています。ChatGPTもその一つです。トークナイザの重要性自然言語処理において、適切なトークナイザーの選択は、モデルの性能向上に大きく貢献します。英語は空白で単語が切られているので分割はしやすいですが、特に日本語のような空白で意味が区切られていない言語では、トークン化の精度が重要です。トークナイザーは、自然言語処理の基盤であり、適切なトークナイズが行われることでモデルのパフォーマンスが飛躍的に向上します。参考文献大規模自然言語モデル(LLM)で利用されるトークナイザーについてLLm(大規模言語モデル)とは?生成AIとの違いや仕組みを解説日本語LLMにおけるトークナイザーの重要性まとめまず識別(トークナイズ)してその後、各トークンに対して型をつけることでコンピュータが処理しやすい形になります。それに関連して生成AIの内部処理やモデル作成について調べることで、よりトークンへの理解が深められるかと思います。 - 2024-09-27
「低レイヤを知りたい人のためのCコンパイラ作成入門」第五回勉強会まとめ
学習範囲ステップ2:加減算のできるコンパイラの作成まとめ前回との違いとして、+(プラス)や-(マイナス)を判別して、加算減算をできるようになった今回の学習内容が完璧にわかるコード振り返り#include <stdio.h>#include <stdlib.h>int main(int argc, char **argv) { // 引数の個数と値を受け取る if (argc != 2) { // 引数の個数が2個でない場合(1個目はファイル名) fprintf(stderr, "引数の個数が正しくありません\n"); return 1; } char *p = argv[1]; // 5+20-4のような文字列を受け取る printf(".intel_syntax noprefix\n"); printf(".globl main\n"); printf("main:\n"); printf(" mov rax, %ld\n", strtol(p, &p, 10)); // 5をraxに代入、pは次の文字列である+を指す while (*p) { if (*p == '+') { p++; // 1文字進める printf(" add rax, %ld\n", strtol(p, &p, 10)); // strtolにより2は20に変換される continue; // +20のような文字列を受け取り、add rax, 20を出力する } if (*p == '-') { p++; printf(" sub rax, %ld\n", strtol(p, &p, 10)); continue; } fprintf(stderr, "予期しない文字です: '%c'\n", *p); return 1; } printf(" ret\n"); // *pが終端文字の場合、retを出力する return 0;}実行結果test.shにassert 22 "5+21-4"と記入make testの実行結果docker run -it -v /Users/ikuzawakazushi/9cc:/9cc -w /9cc compilerbook ./test.sh/usr/bin/ld: warning: /tmp/cc9SCq0c.o: missing .note.GNU-stack section implies executable stack/usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker5+21-4 => 22OK/usr/bin/ld: warning:〜``/usr/bin/ld: NOTE: 〜部分は無視していいらしいどのような時にエラーになるか5 +21-4のように文字の後に空白がある場合は空白を読み込めないのでエラーになる。しかし5+21- 4のように文字の前に空白がある場合はstrtolの仕様で、数字が出るまで空白部分を潰してくれるためエラーにならず、テストが通る。メモatoi→文字ストリングを整数値に変換strtol→文字をlong型に変換する(string to long)。連続する数字部分を1つの数値として解釈される。stderr→標準エラー。何も指定しない場合に、システムで用意してあるエラーを表示。fprintf→printfの出力先が指摘できるようにしたやつ。stdoutを指定すればprintfと挙動は一緒になる。stderrを指定すればエラーを出力する。%ld→10進数として入力するargc→引数の数**argv→argv[0]には必ずパスを含めたファイル名が入る。argv[1]には1つめの引数。今回の場合はargv[1]は5+20-4のこと所感ようやく計算のようなものができるようになり、小さくはあるが達成感があった。C言語で用意されている構文を把握しておかないと、流れが微妙にわからないところがある。strtolで2を読み込んで20に変換してくれることを知らず「2を読み込んだ0読み込んだら+でも-でもないからエラーにならないか?」と思ったので、こういった疑問をもち、構文についての知識を深めよう。 - 2024-09-11
「低レイヤを知りたい人のためのCコンパイラ作成入門」第四回勉強会まとめ
今回の学習範囲自動テストの作成~makeによるビルドまで目標テストをシェルスクリプトで書いて、そのテストコードが正しく通ることを確認する自動テストの作成#!/bin/bashassert() { expected="$1" input="$2" ./9cc "$input" > tmp.s cc -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 0assert 42 42echo OK上記の内容でtest.shを作成します。コードの説明9ccの結果をアセンブルし、実際の結果($actual)を期待されている値($expected)と比較するということを行い、0と42がどちらも正しくコンパイルできることを確認するためのテストです。assert関数は引数を二つ受け取ります。入力した値をそのまま返し、$actualに入れます。./9cc "$input" > tmp.s cc -o tmp tmp.s ./tmp actual="$?"期待する値=$expected(第一引数)と入力した値=$input(第二引数)の実行結果の$actualが等しければ真となります。test.shに実行権限付与作成したtest.shファイルは現在以下のようになっています。オーナーの権限(ファイルの持ち主)は読み取り(Read)と書き込み(Write)のみで実行権限はありません。権限はls -alFコマンドで確認できます。-rw-r--r-- 1 kazu staff 319 9 6 17:52 test.shですのでchmod a+x test.shを実行して実行可能にします。x(execute)追加されて実行可能となりました。-rwxr-xr-x 1 kazu staff 319 9 6 17:52 test.sh*Macを使用している方へ同じディレクトリにMakefileというファイル名で作成してください。毎回dockerコマンドを打つのは正直大変なので、Makefileに記述してコマンドを登録し自動化します。$ docker run -it -v $HOME/9cc:/9cc -w /9cc compilerbook Makefile内CFLAGS=-std=c11 -g -static9cc: 9cc.c docker run -it -v ${CURDIR}:/9cc -w /9cc compilerbook cc -o 9cc 9cc.ctest: 9cc docker run -it -v ${CURDIR}:/9cc -w /9cc compilerbook ./test.shclean: rm -f 9cc *.o *~ tmp*.PHONY: test clean${CURDIR}は現在のディレクトリの現在のパスを取得します。/9cc -w /9ccはdockerのコンテナ内のディレクトリを取得します。これでmake testと打つだけでテストを実行できるようになりました!テストの実行正しいとき$ make testdocker run -it -v /Users/kazu/9cc:/9cc -w /9cc compilerbook ./test.sh0 => 042 => 42OKエラーを起こさせてみます。assertに渡す引数を以下のように変更します。assert 0 0assert 42 123実行してみましょう。1つ目のassert関数は通りましたが、2つ目のassert関数は偽となりました。make testdocker run -it -v /Users/kazu/9cc:/9cc -w /9cc compilerbook ./test.sh0 => 0123 => 42 expected, but got 123シェルスクリプトを記述する上での注意シェルスクリプトはスペースにとても敏感です。以下のような正しいコードで実行すると '[' 0 = 0 ']'となりますが、 if [ "$actual" = "$expected" ]; then$expectedの右のスペースをなくすと '[' 0 = '0]'となり、0がシングルクオーテーションに含まれてしまいます。つまり、0]というコマンドとして認識されてしまうためエラーとなります。スペースや誤字に気をつけましょう。やったことまとめシェルスクリプトでテストコードを記述し、挙動の確認をしました。Makefileに実行したいコマンドを登録し自動化を行いました。所感&メモ自動テストというものは一発で動かして機械的に比較できるくらい簡単なものでいいということでした。難しいことは考えずにとりあえずテストを書くことが大切だということでした。 - 2024-09-05
「低レイヤを知りたい人のためのCコンパイラ作成入門」第三回勉強会まとめ
はじめに下記の内容が分からない場合は第1回に戻りましょう・ジャンプ、アドレスとは?・コンパイル、アセンブルするとは何をさすか?https://www.thehub.co.jp/blogs/making-c-compiler-study-1st本ブログの執筆ルールも第1回の内容を引き継ぎます。第2回はこちらhttps://www.thehub.co.jp/blogs/making-c-compiler-study-2ndざっくりいうと「あるCコードをコンパイルした結果と、それに対応するアセンブリプログラムをアセンブリした結果は当然一致するよね」ということを確認した回でした。今回の学習範囲Cとそれに対応するアセンブラ(関数呼び出しを含む例)〜ステップ1:整数1個をコンパイルする言語の作成(コンパイラ本体の作成)関数呼び出しを含む例関数がある場合のアセンブラのコードの流れについて解説しています。第2回でされたアセンブラのコードの流れの説明の続きです。新しくでてきた言葉をプリングルスで説明。スタック・・・後入先出しをするデータ構造。プリングルスでいうところのプリングルスの筒。データはプリングルス1枚1枚。スタックポインタ・・・スタック内で次のデータが格納されるところ。プリングルスでいうところの、一番上のプリングルス。プッシュ・・・プリングルスを1枚追加して、スタックポインタを1つ上に移動すること。ポップ・・・プリングルスを1枚とりだして、スタックポインタを1つ下に移動すること。コードの実行がなかったため完結な説明に留めます。電卓レベルの実装Cコンパイラの最初のステップとして四則演算の式をコンパイルできるようにするのが目標。ステップ1:整数1個をコンパイルする言語の作成1個の数字の入力を受け付けて、その数字をプログラムの終了として返すアセンブリを出力するコンパイラを作成する。実行した流れ1:数字を入力してアセンブリを出力するコンパイラを作成2:コンパイルを実行して、アセンブリを出力する3:アセンブリを実行して、結果を出力する1. 数字を入力してアセンブリを出力するコンパイラ作成#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(".globl main\n"); printf("main:\n"); printf(" mov rax, %d\n", atoi(argv[1])); printf(" ret\n"); return 0;}#include <stdio.h>はprintfなどC言語の関数を使うために必要なおまじない(厳密には違うがこの勉強会ではこの理解でよいと思う)printfは単に出力するコマンド2. コンパイルを実行して、アセンブリを出力する$ cc -o 9cc 9cc.cmacの場合は$ docker run -it -v $HOME/9cc:/9cc -w /9cc compilerbook cc -o 9cc 9cc.cccは拡張子で言語を判定してコンパイルやアセンブルするコマンド。9cc.cをコンパイルして9ccという実行ファイルを生成。第二回にあるように、macの場合はdocker経由でコマンドを実行する必要がある。(長いので以降は省略)$ ./9cc 123 > tmp.s9ccを使って123を引数にしてtmp.sというファイルを生成。.sはアセンブリの拡張子。実行した結果(アセンブリ)は下記のとおり。$ cat tmp.s.intel_syntax noprefix.globl mainmain: mov rax, 123 ret3. アセンブリを実行して、結果を出力する$ cc -o tmp tmp.s$ ./tmp$ echo $?123ccでtmp.sをアセンブルしてtmpファイルを生成。シェルでは直前のコマンドの終了コードが$?という変数でアクセスできる。9ccで与えた引数である123が出力されればうまくいっているということになる。やったことまとめ関数を実行する時のアセンブラのコードの流れを確認しました整数を入力して、その数を出力するコンパイラを作成しました所感&メモ専門単語が多く、すぐには理解できませんでしたが、第1回第2回の復習をしてから読み直したら理解できました。本書内で必要な言葉は全て解説されています。 - 2024-08-09
「低レイヤを知りたい人のためのCコンパイラ作成入門」第二回勉強会まとめ
勉強した範囲macOSでCコンパイラを作成するための開発環境の構築Cとそれに対応するアセンブラ〜アセンブリプログラムの実行所感&メモWindowsはWindows Subsystem for Linux(WSL)というアプリケーションをインストールすることで、Linux互換環境を容易に準備できる。macOSはLinuxとアセンブリのソースレベルでは完全互換ではないため、仮想環境を使ってLinuxを用意したほうがいい。私はmacOSを使用しているので、Dockerで開発環境の構築を行った。Dockerを使用した理由: この勉強会の趣旨はCコンパイラ作成のテクニックを学ぶためのものだが、細かな点で非互換性に悩まされ時間を使うのは本末転倒であるため。実行したコマンドセットアップまず、Dockerfile からイメージを 構築(build)する。docker build -t compilerbook https://www.sigbus.info/compilerbook/Dockerfilebuildが完了したので、lsコマンドでコンテナの中身を確認してみる。docker run --rm compilerbook ls /binbootdevetchomelibmediamntoptprocrootrunsbinsrvsystmpusrvarコンテナ内でシェルを起動させるコマンドdocker run -it -v $HOME/9cc:/9cc -w /9cc compilerbookオプション説明docker run -it -v [ボリューム(ホストOSのパス)]:[マウント先(コンテナ内のパス)] -w /[実行をするコンテナ内のディレクトリを指定]オプション-itコンテナ内でシェルを起動して、対話形式で使用する-v現在の作業ディレクトリをコンテナ内にマウント(ホストOSとコンテナ間でデータを共有)する。-w現在の作業用ディレクトリの中で実行されるように指示C言語で書かれたプログラムを用意〜/9cc/test1.c内に以下のプログラムを作成。int main() { return 42;}int:42という整数を返すのでint型(ineger)を宣言。main:全体を管理する機能を持った関数。プログラム内で最初に呼ばれる特別な関数。コンパイルコンピュータはC言語で書かれたプログラム(ソースプログラム)を直接実行することはできないので、直接実行可能な「機械語命令(アセンブリ言語)」に変換する必要がある。これをコンパイルという。先ほどのソースプログラムtest1.cをコンパイルする。docker run -it -v $HOME/9cc:/9cc -w /9cc compilerbook cc -o test1 test1.cオプションccコンパイルの設定-o出力ファイルに (デフォルトの a.out の代わりに) <出力ファイル> という名前を付ける。コンパイルによってできた実行ファイルが存在するか確認test1という実行ファイルが作成されている。docker run -it -v $HOME/9cc:/9cc -w /9cc compilerbookuser@db2d9edc013e:/9cc$ ls9cc test1 test1.ctest1の中身は、機械語命令に対応するアセンブリが表示されている。user@93b91a9a80fa:/9cc$ objdump -d -M intel ./test1./test1: file format elf64-littleaarch64Disassembly of section .init:0000000000000580 <_init>: 580: objdump: unrecognised disassembler option: inteld503201f nop 584: a9bf7bfd stp x29, x30, [sp, #-16]! 588: 910003fd mov x29, sp 58c: 9400002a bl 634 <call_weak_fn> 590: a8c17bfd ldp x29, x30, [sp], #16 594: d65f03c0 retDisassembly of section .plt:00000000000005a0 <.plt>: 5a0: a9bf7bf0 stp x16, x30, [sp, #-16]! 5a4: f00000f0 adrp x16, 1f000 <__FRAME_END__+0x1e7f4> 5a8: f947d611 ldr x17, [x16, #4008] 5ac: 913ea210 add x16, x16, #0xfa8 5b0: d61f0220 br x17 5b4: d503201f nop 5b8: d503201f nop 5bc: d503201f nop00000000000005c0 <__libc_start_main@plt>: 5c0: f00000f0 adrp x16, 1f000 <__FRAME_END__+0x1e7f4> 5c4: f947da11 ldr x17, [x16, #4016] 5c8: 913ec210 add x16, x16, #0xfb0 5cc: d61f0220 br x1700000000000005d0 <__cxa_finalize@plt>: 5d0: f00000f0 adrp x16, 1f000 <__FRAME_END__+0x1e7f4> 5d4: f947de11 ldr x17, [x16, #4024] 5d8: 913ee210 add x16, x16, #0xfb8 5dc: d61f0220 br x1700000000000005e0 <__gmon_start__@plt>: 5e0: f00000f0 adrp x16, 1f000 <__FRAME_END__+0x1e7f4> 5e4: f947e211 ldr x17, [x16, #4032] 5e8: 913f0210 add x16, x16, #0xfc0 5ec: d61f0220 br x1700000000000005f0 <abort@plt>: 5f0: f00000f0 adrp x16, 1f000 <__FRAME_END__+0x1e7f4> 5f4: f947e611 ldr x17, [x16, #4040] 5f8: 913f2210 add x16, x16, #0xfc8 5fc: d61f0220 br x17Disassembly of section .text:0000000000000600 <_start>: 600: d503201f nop 604: d280001d mov x29, #0x0 // #0 608: d280001e mov x30, #0x0 // #0 60c: aa0003e5 mov x5, x0 610: f94003e1 ldr x1, [sp] 614: 910023e2 add x2, sp, #0x8 618: 910003e6 mov x6, sp 61c: f00000e0 adrp x0, 1f000 <__FRAME_END__+0x1e7f4> 620: f947f800 ldr x0, [x0, #4080] 624: d2800003 mov x3, #0x0 // #0 628: d2800004 mov x4, #0x0 // #0 62c: 97ffffe5 bl 5c0 <__libc_start_main@plt> 630: 97fffff0 bl 5f0 <abort@plt>0000000000000634 <call_weak_fn>: 634: f00000e0 adrp x0, 1f000 <__FRAME_END__+0x1e7f4> 638: f947f400 ldr x0, [x0, #4072] 63c: b4000040 cbz x0, 644 <call_weak_fn+0x10> 640: 17ffffe8 b 5e0 <__gmon_start__@plt> 644: d65f03c0 ret 648: d503201f nop 64c: d503201f nop0000000000000650 <deregister_tm_clones>: 650: 90000100 adrp x0, 20000 <__data_start> 654: 91004000 add x0, x0, #0x10 658: 90000101 adrp x1, 20000 <__data_start> 65c: 91004021 add x1, x1, #0x10 660: eb00003f cmp x1, x0 664: 540000c0 b.eq 67c <deregister_tm_clones+0x2c> // b.none 668: f00000e1 adrp x1, 1f000 <__FRAME_END__+0x1e7f4> 66c: f947ec21 ldr x1, [x1, #4056] 670: b4000061 cbz x1, 67c <deregister_tm_clones+0x2c> 674: aa0103f0 mov x16, x1 678: d61f0200 br x16 67c: d65f03c0 ret0000000000000680 <register_tm_clones>: 680: 90000100 adrp x0, 20000 <__data_start> 684: 91004000 add x0, x0, #0x10 688: 90000101 adrp x1, 20000 <__data_start> 68c: 91004021 add x1, x1, #0x10 690: cb000021 sub x1, x1, x0 694: d37ffc22 lsr x2, x1, #63 698: 8b810c41 add x1, x2, x1, asr #3 69c: 9341fc21 asr x1, x1, #1 6a0: b40000c1 cbz x1, 6b8 <register_tm_clones+0x38> 6a4: f00000e2 adrp x2, 1f000 <__FRAME_END__+0x1e7f4> 6a8: f947fc42 ldr x2, [x2, #4088] 6ac: b4000062 cbz x2, 6b8 <register_tm_clones+0x38> 6b0: aa0203f0 mov x16, x2 6b4: d61f0200 br x16 6b8: d65f03c0 ret 6bc: d503201f nop00000000000006c0 <__do_global_dtors_aux>: 6c0: a9be7bfd stp x29, x30, [sp, #-32]! 6c4: 910003fd mov x29, sp 6c8: f9000bf3 str x19, [sp, #16] 6cc: 90000113 adrp x19, 20000 <__data_start> 6d0: 39404260 ldrb w0, [x19, #16] 6d4: 37000140 tbnz w0, #0, 6fc <__do_global_dtors_aux+0x3c> 6d8: f00000e0 adrp x0, 1f000 <__FRAME_END__+0x1e7f4> 6dc: f947f000 ldr x0, [x0, #4064] 6e0: b4000080 cbz x0, 6f0 <__do_global_dtors_aux+0x30> 6e4: 90000100 adrp x0, 20000 <__data_start> 6e8: f9400400 ldr x0, [x0, #8] 6ec: 97ffffb9 bl 5d0 <__cxa_finalize@plt> 6f0: 97ffffd8 bl 650 <deregister_tm_clones> 6f4: 52800020 mov w0, #0x1 // #1 6f8: 39004260 strb w0, [x19, #16] 6fc: f9400bf3 ldr x19, [sp, #16] 700: a8c27bfd ldp x29, x30, [sp], #32 704: d65f03c0 ret 708: d503201f nop 70c: d503201f nop0000000000000710 <frame_dummy>: 710: 17ffffdc b 680 <register_tm_clones>0000000000000714 <main>: 714: 52800540 mov w0, #0x2a // #42 718: d65f03c0 retDisassembly of section .fini:000000000000071c <_fini>: 71c: d503201f nop 720: a9bf7bfd stp x29, x30, [sp, #-16]! 724: 910003fd mov x29, sp 728: a8c17bfd ldp x29, x30, [sp], #16 72c: d65f03c0 ret出力結果コンパイルによってできた実行ファイルを実行user@6a6a8f30e625:/9cc$ ./test1コマンド終了直後に$?をechoで表示することで、そのコマンドの終了結果を確認できる。正しく42が返されている。user@6a6a8f30e625:/9cc$ echo $?42今回のまとめDockerのオプションの意味を深く理解できる機会となった。ccコマンドにより、C言語のソースプログラムがコンピュータが理解できる「機械語命令(アセンブリ言語)」に変換される。 - 2024-08-05
「低レイヤを知りたい人のためのCコンパイラ作成入門」第一回勉強会まとめ
この振り返りの内容の目的としては、この振り返りをみたら勉強会の内容を思い出せるように、思い返しの手掛かりになるような振り返りを書くことです。振り返りの形式について勉強会の振り返りを書くにあたって記述の形式を非常に迷いました。例えばただコピペするならほとんど価値はないし、メモの内容だけでは何をしたのかがわかりづらいからです。価値のある振り返りにするために、自分の書き方でははじめにいくつか方針を決めておいて、その方針に従って書いていこうと思います。以下の条件で書きはじめますが、何回か書いていくうちにアップデートされるのではないかと思います。以下「低レイヤを知りたい人のためのCコンパイラ作成入門」を「本書」、この振り返り記事のことを「振り返り」と呼びます。本書の目的「C言語で書かれたソースコードをアセンブリ言語に変換するプログラム、つまりCコンパイラを作成」のために必要な振り返りのみをする。コラムや著者には言及しない。長々と振り返りをしてしまっては思い返すための手掛かりにならず、本書を読み返した方がよい。振り返りは手短に2000文字前後に収める。実際に手を動かして実行したコマンドが毎回あるはずなので、その振り返りを中心にする。理由としてはエピソード記憶の方が「付箋のような役割」になりやすいため。それ以外のまとめは先に箇条書きでかくので、それを見て思い出せなければ本書を読み返す。勉強した範囲はじめに(全て)〜機械語とアセンブラ(CPUとメモリ)今回まとめ本書の目標はC言語で書かれたソースコードをアセンブリ言語に変換するプログラム(Cコンパイラ)を作成すること。そのために当面は、自作コンパイラでコンパイルできることを目標とする。最初の方は「独自言語」を実装して、その言語に機能を追加していってCと一致するものを作成していく。本書では細かいコミットをつくってコンパイラを作成していくが、どのコミットにおいてもコンパイラはある意味常に「完成形」になる。どの段階でも、その時点の完成度に合わせたリーズナブルな仕様の言語を目指す。「CPUとメモリ」の項コンピュータを構成するコンポーネントであるCPUとメモリについての説明している。下記の言語について説明があるので、わからない言葉があれば読み返す。CPU,アドレス,プログラムカウンタ(PC),機械語,分岐命令,レジスタ,命令アーキテクチャ機械語は人間にとって扱いやすいものではないため、より読みやすいアセンブリ言語を開発した。アセンブリは機械語と1対1で対応している。アセンブルするとはアセンブリを機械語に変換することである。所感&メモ「コンパイル」「アセンブル」が何を何に変換するものなのか、正確にわかっていた方がよさそう。ソースコード→アセンブリ言語に変換するのが「コンパイル」、アセンブリ言語→機械語に変換するのが「アセンブル」という(コンパイルと呼ぶときもあるらしい)「CPUとメモリ」の項は「CPU」や「プログラムカウンタ」などこれから繰り返し出てくる単語の説明があるので、わからなくなったらすぐ戻って読み直したほうが良さそう。井原さんの例えで言うと、メモリは食材を入れる冷蔵庫、レジスタはまな板、CPUは調理することに該当するらしい。ある言語のコンパイルをするために、最初は別の言語でコンパイルする必要がある?らしい。コンパイルしている言語をコンパイルしていた言語・・・というふうに辿っていくと始祖言語にたどり着くという話が興味深かった。実行したコマンドobjdump -d -M intel /bin/ls何のコマンドか?/bin/lsの内容をIntel形式のアセンブリ言語で表示するコマンドコマンドの解説objdump:指定したオブジェクトファイルの中身を表示するコマンド-d:逆アセンブル(機械語をアセンブリに変換)して表示するオプション-M intel:Intel形式のアセンブリ言語での出力を指定する出力結果$ objdump -d -M intel /bin/ls/bin/ls: file format elf64-x86-64Disassembly of section .init:0000000000003d58 <_init@@Base>: 3d58: 48 83 ec 08 sub rsp,0x8 3d5c: 48 8b 05 7d b9 21 00 mov rax,QWORD PTR [rip+0x21b97d] 3d63: 48 85 c0 test rax,rax 3d66: 74 02 je 366a <_init@@Base+0x12> 3d68: ff d0 call rax 3d6a: 48 83 c4 08 add rsp,0x8 3d6e: c3 ret...3d58: 48 83 ec 08 sub rsp,0x83d58が機械語の入っているメモリのアドレス、その次に続いている4つの16進数の数値は実際の機械語、sub rsp,0x8というのは、その機械語命令に対応するアセンブリ。この命令は、RSPというレジスタから8を引く(subtract = 引く)という命令になる。 - 2024-08-01
なぜ「低レイヤを知りたい人のためのCコンパイラ作成入門」を勉強会の題材として実践するのか
TL;DRコンパイラ作成は楽しいから記事を書くことになった理由THEHUBという会社でエンジニアリング勉強会として週に1度、1時間 低レイヤを知りたい人のためのCコンパイラ作成入門 を業務時間に進めていく活動を始めたそこで講師をしているが、なんでエンジニアリング勉強会を開催しているか、題材としてコンパイラ作成入門を選んだかなどを記しておくエンジニアリング勉強なので、参加者はエンジニアだけじゃなく事業開発メンバーもWSLで参加しているなんでエンジニアリング勉強会を開催するのかエンジニアとしてこの先生き残るための術を身につけるとbetter人に教えることで自分の理解度を高めたい仲間がいれば長く続けられるのでbetterエンジニアとしてこの先生き残るための術を身につけるとbetter偉大なるt-wada氏がすべてを説明してくれている最強の資料があるのでそちらをどうぞEmbedded content: https://speakerdeck.com/rtechkouhou/enziniatositekofalsexian-sheng-kifalsekorutameni仕事に追われるときこりのジレンマしていってしまうので、仕事としてきこりのジレンマを打破していこう人に教えることで自分の理解度を高めたい人に教えると自分の理解度も飛躍的に高まるのでおすすめです自分が知らないことをよく知っている人に教えてもらうという方針で勉強会やってたりするコミュニティもあった気がするのでおすすめです仲間がいれば長く続けられるのでbetter現代では娯楽がいっぱいあるので1人だと誘惑に負けてしまう、人間なのでしかし、1人でも続けるのが大変なことも一緒に実践する仲間がいれば続けられる!とか思っていたら、Vimのプラグイン開発者のShougoさんがちょうどタイムリーな投稿をしていたので引用Embedded content: https://x.com/ShougoMatsu/status/1821111963113681404もちろん、1人でもできる人は1人でやってもOKなんでCコンパイラ作成入門なのかコンパイラ作成は楽しい技術選定の審美眼が身につくとbetterシステム開発のやり方が身につくとbetter良い記事なので布教したいコンパイラ作成は楽しいコンパイラ作成は大変楽しい作業です。最初のころはバカバカしいくらい単純なことしかできなかった自作言語が、開発を続けていくとたちまちのうちに自分でも驚くくらいC言語っぽく成長していって、まるで魔法のようにうまく動くようになります。実際に開発をしてみると、その時点でうまくコンパイルできるとは思えない大きめのテストコードがエラーなしにコンパイルできて、完全に正しく動くことに驚くことがよくあります。そういうコードはコンパイル結果のアセンブリを見ても自分ではすぐには理解できません。時折、自作のコンパイラが作者である自分を超える知性を持っているように感じることすらあります。コンパイラは仕組みがわかっていても、どことなく、なぜここまでうまく動くのか不思議な感じがするプログラムです。きっとあなたもその魅力に夢中になることでしょう。低レイヤを知りたい人のためのCコンパイラ作成入門: はじめに より引用実際にやってみると人類の叡智と歴史を感じられて本当に楽しかった技術選定の審美眼が身につくとbetterまたもやt-wada氏がすべてを説明してくれている最強の資料があるのでそちらをどうぞEmbedded content: https://speakerdeck.com/twada/understanding-the-spiral-of-technologies-2023-edition技術の流行り廃りは色々あるが、根本の動作原理は今でも変わっておらず、その技術は当然Webシステム開発にも役に立つシステム開発のやり方が身につくとbetterまた、この本は、大きなプログラムを1から書くにはどうすればよいのかということを説明している本でもあります。大きなプログラムを作るスキルというのは、データ構造やアルゴリズムを学ぶのとはまた違った一種独特のスキルなのですが、そういったものを解説している本はあまりないように思います。また、仮に解説してもらっても、実際に体験してみなければ、開発手法の良し悪しというものはよくわからないものです。本書は、自作言語をC言語に育てていくプロセスが、一つの良い開発手法の実体験になるようにデザインされています。低レイヤを知りたい人のためのCコンパイラ作成入門: はじめに より引用これは当然大きなWebシステム開発にも適用できるし、もっと言えばすべての仕事にも当てはまる良い記事なので布教したいこれが推しを布教する心なんだと理解できる蛇足木こりのジレンマを打破するためにやれることはいっぱいあるが、仕事として一番触れている時間が長くて楽しいのはプログラミング言語作成かと思い、それをやることにした自分は crafting interpreters をやってから 低レイヤを知りたい人のためのCコンパイラ作成入門 をやったこのときの経験はどちらも良かったが、Cコンパイラ作成の方が直接的にパソコンの動作理解に役に立ったので今回はそっちをやることにしたcrafting interpretersも良い記事なので興味があればぜひやってほしいおしまい - 2023-06-19
デジタル技術の活用及びDX推進の取組状況について
1. 経営の方向性及びデジタル技術等の活用の方向性1-1. デジタル技術が社会や弊社の競争環境に及ぼす影響について当社は設立以来、主にスタートアップ・ベンチャー企業に対する課題解決ITプロダクトの開発を継続的に行っています。ITプロダクト開発の分野では、ChatGTPに代表されるAIツールなどを普及や多様化により、益々デジタル技術の活用が不可欠となっており、最先端デジタル技術に関する習熟や活用が自社の発展や生産性向上に必要不可欠と認識しています。1-2. 経営ビジョンやビジネスモデル当社は「訪問歯科を正しく。患者様に向き合える時間をたくさん創る」というミッションのもとに経営されています。 隆盛する訪問歯科におけるグレーゾーンや、法律に業務実態が追いついていない部分を可視化し、歯科医院が訪問を適切に正しく行えるように支援して参ります。当社は「エンジニアの創出価値を最大化する」というビジョンのもとに経営されています。ITシステム開発の分野では、従来のようなオフィスに集まり対面でのコミュニケーションをして仕事を進める文化だけでなく、在宅勤務・リモートワークを前提とした業務設計が進んでいます。また、当社はフリーランスや副業人材とのネットワークを駆使した生産性の高いDX化を推進することに取り組んでいます、2.経営およびデジタル技術等の活用の具体的な方策・戦略の決定弊社はミッション達成のためにリモートワークONLY業務委託契約をメインにというルールを掲げてビジネスに取り組んでいます。全ての業務上のやりとりをオンラインで行い、オンライン中心の業務を設計することで、優秀なメンバーがより高い生産性を発揮できるように努めます。また、メンバーの成長を素早く報酬に反映するべく、採用メンバーは業務委託契約をメインに結ぶようにしています。一部のジュニアメンバー(育成対象層)を除き、優秀なメンバーが常に報酬に対するギャップを感じることなく満足して高い生産性を発揮できる体制を作り続けます。3. 戦略を効率的に進めるための体制の提示本事業では代表として康裕三をCEO(最高責任者)とし、以下の体制で新たなビジネスモデルを実現します。氏名役職本事業における役割康 裕三代表取締役CEO井原 大貴取締役CTO