Sequelizeを簡単に試せる環境を構築する。

Sequelizeはnode用のORマッパー。expressでWebアプリを組むときには選択肢に上がってくる。

このSequelize(に限らないが)、手を動かさないとよくわからないところがあったりするので、お手軽に試せる環境を作った。

github.com

docker-composeを使って、mysqlのコンテナとnodeのコンテナを立てて実行する感じになっている。 サンプルとして適当なテーブルを切って、簡単なリレーションの設定や、外部キー制約などを試している。

nodeはコンテナとしての実行と、ローカルでの実行の両方に対応させておいた。 環境構築済みならローカルのほうが試行錯誤はしやすい。

手っ取り早くTypescript環境を作って実行する。

ちょっとした動作を手元で試したいときに。 nodeとかyarnはインストール済みの前提です。

環境構築

Yarn

mkdir ts-study
cd ts-study
yarn init -y
yarn add -D typescript ts-node @types/node

npmの場合

mkdir ts-study
cd ts-study
npm init -y
npm install -D typescript ts-node @types/node

tsconfig.jsonはなくてもとりあえず動くけど、作ってtargetだけは適切に設定したほうがよい。 (ESNEXTの部分は環境にあわせて変更)

yarn run tsc -t ESNEXT --init

npmの場合は

npx tsc -t ESNEXT --init

サンプルコード

echo 'console.log("Hello, World")' >> main.ts

実行

yarnの場合

yarn run ts-node main.ts

npmの場合

npx ts-node main.ts

お手軽とはいえないが、我慢できなくもない、かな。 ちょっとした確認なら、tsじゃなくてもええんちゃう、という話もあるが。

Javaをサーバで実行するときの注意点

Windows限定だと思うのだが、サーバでJavaで書いたプログラムをRDPで実行した後、RDPをログアウトすると動かなくなる。 再度ログインして、ターミナルを操作しようとすると復帰するが、明らかに停止している。

これは、ログアウトのシグナルによって、Javaのプログラムが止められている。 このような場合には、-Xrsオプションをつけて実行するとログアウトしてもJavaプログラムが止まることはなくなった。 シグナルを無視するようになるらしい。

サーバプログラムは、システムにユーザがログインしなくてもサーブしてほしい。

自作Cコンパイラでセルフホストを達成した。

Rui先生の低レイヤを知りたい人のためのCコンパイラ作成入門を参考に作成していたCコンパイラ、Hello, Worldできたら結構満足して休憩していたけど、再開してなんとかセルフホストまでたどり着いた。

いつセルフホストできるようになるのかは全然わからなくなって、途中結構つらかった。

はまったとき他の人のブログとかを参考にしたので、感想や考えていた方針などを一応書き残しておく。

主な進捗の振り返り

  • 2019年12月9日。最初のコミット。
  • 2020年2月10日。Hello, World!が実行できた。
  • 2020年2月15日から2020年の8月13日までは,ちょっと飽きて一旦休憩。
  • 2020年9月19日。セルフホストができた。

ma38su.hatenablog.com

Hello, Worldからセルフホストまで一か月くらい?全体で3ヶ月くらいか。

まずはセルフホストを目指す

とりあえずは最短でセルフホストを目指すのがよい気がする。 なぜかというと、第一世代のテストは常にテストが通る状態を維持できるけれど、 セルフホストのテストは、セルフホストできるまで通らない状態が続く。 この段階でいろいろ機能を追加していくと、デバッグ対象が増えて大変な気がする。

まぁ、セルフホストできないのは機能が足りてないこともあるので、 何が最短なのかを判断するのもそれなりに難しかったけど。

例えば、当初、signed、unsignedはTokenizeの段階で読み飛ばしたのだが、 セルフホストできない理由は、unsignedを読んでいないからかも? 符号拡張とゼロ拡張を適当にやっているのは危険かも?と不安になって実装した。 (が、chibiccとはではunsigned扱ってないみたいなので、いらんのかも)

ヘッダファイルの扱い

普通のC言語のコードを、できるだけそのままコンパイルできるようにしたかったので、 ヘッダファイルを読んでコンパイルするようにした。

ヘッダファイルを利用したC言語コンパイルするにあたっては、 C言語標準ライブラリのlibcとリンクするか、libcも実装する必要がある。 すると、手加減なしで、typedefとか、static const unsigned signedなどの型定義がでてくる。 この辺読み飛ばしたり、適当にごまかしたりするのがそこそこ骨が折れた。 (大体は読み飛ばしてよい)

それだけではなくて、ヘッダファイル中には、 GCC依存?のbuiltin関数を使ったマクロなども結構埋まっていて、 そのあたりの対処もめんどくさかった。 とはいえ自分でプリプロセッサ書くよりは楽だと思うけど。

GCC互換をどこまで求めるか?

セルフホストを目的とするとき、GCCと全く同じ動きをする必要は必ずしもない。 (とセルフホストできたから言い切れるだけだけど。)

例えば、途中、ローカル変数の配置がGCCと逆順になっていることに気づいたが、 原理的には問題ないと思って、そのままにした。 (これ構造体のメンバの配置だったら当然まずいんだけど)

ただまぁ、うまくいかないときは、 寄せたほうがトラブルは少ないのも確かなので安易にGCCに寄せる判断をしがち。

ハマったところ

よく言われている、第一世代では通るテストが第二世代だとコンパイルできないということは、 やっぱりあってここが最も苦労した。そんなデバッグ方法のノウハウは普通持ち合わせていない。

人によってはまりどころは違いそうだけれど。

関数フレームサイズの決定

これコンパイラ組んでみて、とてもよく理解できたことなんだけど、深く考えずミスってしまったところでもある。

自分の場合は、スコープの取り扱いというか、関数フレームサイズの計算が間違っていて、 関数呼び出し時などに、ローカル変数が上書きしてしまうことを稀に起こしてしまっていた。 これなかなか原因に到達できず苦労した。

ただ、関数フレームのサイズ間違えても意外と動くので、普通に(C言語の)テスト書いていても検出できないことも多い。

スタックは積みっぱなしにしない。

スタックマシン?なれると便利で気軽に積んでいくんだけど、スタックに積んだものはちゃんと使いきるか、あまっても使いきれるように計画的に処理する必要がある。 スタックに積んだけどやっぱりいらなかったみたいなことはありうるが、その時ほったらかすと、 スタックポインタがずれておかしなことになることもありうる。

これも、意外と動くので、普通に(C言語の)テスト書いていても検出できないことも多い。

デバッグ方法

ここからはTIPS的なところ。

低レイヤ~で紹介されているerrorとerror_atの実装が便利なので、これで大体足りる。 他にもいくつかできることはあるので紹介する。

アセンブラにコメントを出力する。

ただ、これはアセンブラを開いて確認しないと気付かない。

printf(" # coment\n");

標準エラー出力を使う

標準出力でアセンブラを吐くので、デバッグには標準エラー出力を使う。

fprintf(stderr, "DEBUG");

GDBを使う

SEGV箇所の特定にはgdbが便利。

gdbを使ってコンパイル済みの実行ファイルを指定し,引数を指定して実行すればどの関数でSEGVしたか教えてくれる。

./gdb ./38cc
(gdb) run 引数

あまり積極的には使っていないのだけれど、ほかにもいろいろできるらしい。 ただ、第二世代のときはgdbでできることも限られるらしい。

セルフホストを終えて

セルフホストできた日は達成感とか高揚感があって一日気分よく過ごせた。 フルマラソンを歩いてでもなんとか完走したような感じに近いかな。

ただ、もうちょっと低レイヤの理解を深めるにはやっぱりOS書かないとダメかなぁと思っている。

追記

OSも書いてみたけど、まだまだわからない。とりあえずリンカの理解が必要な気がしている。

ma38su.hatenablog.com ma38su.hatenablog.com

アセンブラでHello, World!

書けるようになったので、記念に書いておく。

  .intel_syntax noprefix
  .data
.LC0:
  .string "Hello, World!\n"
  .text
  .global main
main:
  push rbp
  mov rbp, rsp

  lea rdi, .LC0[rip]

  mov rax, 0
  call printf@PLT

  mov rax, 0
  mov rsp, rbp
  pop rbp
  ret

アドレスを指定したデータの転送(Intel記法)

アドレスを指定したデータの転送についても、少しはわかってきた気がするので、まとめる。

動作確認のための数値

前提として、アセンブラの動作確認のため、1073606947825の値を用いた。 この数値を16進数に直すと0xF9F7F5F3F1であり、各バイト毎の値を順に変えている。 こうしておくと、一部のバイトのみ転送する場合に、どのバイトが転送されたのか確認しやすい。

また、この数では、8byte, 4byte 2byteを切り出したときに最上位bitが1になるので、 ゼロ拡張されるのか、符号拡張されるのかを確認しやすい。

レジスタからアドレスへの転送の基本

レジスタraxの指す値へbyte数を指定して転送する。 ちなみにbyte ptrは1byteのみ転送、word ptrは2byteのみ転送、dword ptrは4byteのみ転送、qword ptrは8byteすべて転送する(指定しないのと同様)。

数値によって、即値で転送できる場合とできない場合(0xF9F7F5F3F1はqword ptrには即値で転送できなかったが、qword ptrにも0などの即値なら転送できた。)があったので、rdiに即値を転送してから、rdiから[rax]へ転送するようにした。

  # 0xF9F7F5F3F1
  mov rdi, 1073606947825

  # raxからのアドレス1byteに、0x00000000000000F1を転送する
  mov byte ptr [rax], dil

  # raxからのアドレス2byteの範囲に、0x000000000000F3F1を転送する
  mov word ptr [rax], di # 0xF9F7F5F3F1

  # raxからのアドレス4byteの範囲に、0x00000000F7F5F3F1を転送する
  mov dword ptr [rax], edi

  # raxからのアドレス8byteの範囲に、0x000000F9F7F5F3F1を転送する
  mov qword ptr [rax], rdi

アドレスの確認

転送先のoffsetを変えて、アドレスの起点を確認してみた。

rax+1すると、raxに格納される値は左に1byte分シフトした。

  # 0xF9F7F5F3F1
  mov rdi, 1073606947825

  # raxのアドレスから8byteは、0x0000000000000000になる。
  mov byte ptr [rax-1], dil

  # raxのアドレスから8byteは、0x00000000000000F3になる。
  mov word ptr [rax-1], di # 0xF9F7F5F3F1

  # raxのアドレスから8byteは、0x000000000000F5F3になる。
  mov dword ptr [rax-1], edi

  # raxのアドレスから8byteは、0x00000000F9F7F5F3になる。
  mov qword ptr [rax-1], rdi

rax-1すると、raxに格納される値は右に1byte分シフトした。

  # 0xF9F7F5F3F1
  mov rdi, 1073606947825

  # raxのアドレスから8byteは、0x000000000000F100になる。
  mov byte ptr [rax+1], dil

  # raxのアドレスから8byteは、0x0000000000F2F100になる。
  mov word ptr [rax+1], di # 0xF9F7F5F3F1

  # raxのアドレスから8byteは、0x000000F7F5F3F100になる。
  mov dword ptr [rax+1], edi

  # raxのアドレスから8byteは、0x0000F9F7F5F3F100になる。
  mov qword ptr [rax+1], rdi

ちなみに、sourceに指定したレジスタのサイズと転送先のレジスタのサイズは同じにする必要がある。 dword ptr [rax]なら、8byteレジスタのrdiではなく4byteレジスタのediにする必要がある。

そのため、転送先のサイズを指定せずに即値をアドレスに転送することもできない。 例えば以下はだめ。

  mov [rax], 0x000000F7F5F3F100

アドレスからレジスタへの転送

とりあえず転送の範囲などを確認するため、[rax]の値と、rsiの値を初期化しておく。

  # [rax]を0x000000F9F7F5F3F1としておく
  mov rdi, 1073606947825
  mov [rax], rdi

  # rsiを0xAAAAAAAAAAAAAAAAとしておく
  mov rsi, 12297829382473034410

12297829382473034410は16進数に直すと0xAAAAAAAAAAAAAAAAである。

1byteの転送を以下のコードで確認すると、 rsiの値は0xAAAAAAAAAAAAAAF1となった。 レジスタへの転送のときはptrを指定しなくても転送先のレジスタサイズのみ転送される。

  mov sil, [rax]

2byteの転送を以下のコードで確認すると、 rsiの値は0xAAAAAAAAAAAAF3F1となった。

  mov si, [rax]

4byteの転送を以下のコードで確認すると、 rsiの値は0x00000000F7F5F3F1となった。

  mov esi, [rax]

4byteの転送のみ、レジスタの上位ビットがゼロ拡張されている。 これはeaxにデータ転送したとき、上位4byteはゼロ拡張されるためである。

ちなみに、上位ビットを符号拡張したい場合はmovsxまたはmovsxd命令を使って、rsiまたはesiに転送すればよい。 rsiに転送すれば、8byteの範囲全体に符号拡張し、esiに転送すれば4byteの範囲のみ符号拡張する。 符号拡張する場合は、movによる転送と異なり、転送先のサイズは符号拡張後サイズになるので、転送するサイズはptrで指定する必要がある。

8byteに転送して符号拡張するときは以下

  # 2byteを8byteに転送して符号拡張する 0xFFFFFFFFFFFFFFF1
  movsx rsi, byte ptr [rax]

  # 2byteを8byteに転送して符号拡張する 0xFFFFFFFFFFFFF3F1
  movsx rsi, word ptr [rax]

  # 4byteを8byteに転送して符号拡張する 0xFFFFFFFFF7F5F3F1
  movsx rsi, dword ptr [rax]

4byteに転送して符号拡張するときは以下

  # 2byteを4byteに転送して符号拡張する 0x00000000FFFFFFF1
  movsx esi, byte ptr [rax]

  # 2byteを4byteに転送して符号拡張する 0x00000000FFFFF3F1
  movsx esi, word ptr [rax]

8byteに転送してゼロ拡張するときは以下。転送先をrsiにしても1byteと2byteは動作した。 4byteをゼロ拡張するときは、movでesiに転送すれば勝手に上位4byteはゼロ拡張というかゼロに初期化される。 4byteだけゼロ拡張というのは試した範囲ではできなかったが、、、

  # 1byteを4byteに転送してゼロ拡張する 0x00000000000000F1
  movzx esi, byte ptr [rax]

  # 2byteを8byteに転送してゼロ拡張する 0x000000000000F3F1
  movzx esi, word ptr [rax]

  # 4byteを8byteに転送してゼロ拡張する 0xFFFFFFFFF7F5F3F1
  mov esi, [rax]

拡張するキャスト(Intel記法のアセンブラ)

よくわかってなくてだいぶはまったが、やっとわかったのでまとめておく。 x86_64のCPUを前提とする。

64bitレジスタでのキャストと32bitレジスタ

64bitレジスタで符号拡張するキャスト

符号拡張するキャストというのは、要はsigned型へのキャストすること。

1byteと2byteはmovsxで、4byteのみmovsxdを使う。

# 1byteにキャスト
movsx rax, al

# 2byteにキャスト
movsx rax, ax

# 4byteにキャスト
movsxd rax, eax

64bitレジスタでゼロ拡張するキャスト

ゼロ拡張するキャストというのは、要はunsigned型へのキャストすること。

8byteの値をゼロ拡張して1byteと2byteへのキャストしたい場合はmovzxで64bitレジスタに転送すればよい。

例えば以下でよい。

# 8byteの値を1byteの値にゼロ拡張してキャストする
movzx rax, al

# 8byteの値を2byteの値にキャストする
movzx rax, ax

8byteの値をゼロ拡張して4byteへのキャストしたい場合、movzxではできない。 ではどうするかというと、movで4byteレジスタから4byteレジスタに転送してやればよい。

例えば以下。

# 8byteの値を4byteの値にキャストする
mov eax, eax

これでうまくいく理由は32bitレジスタにデータを転送すると暗黙的に上位ビットがゼロ拡張されるため。

サンプルコード

この辺の挙動を確認するためのアセンブラを書いた。

  .intel_syntax noprefix
  .data
.LC0:
  .string "%lX\n"
  .text
  .global main
main:
  push rbp
  mov rbp, rsp

  mov rax, 68719476735  # 0xFFFFFFFF
  mov eax, eax  # cast to unsigned 4byte type
  push rax

  pop rsi  # arg 1

  lea rdi, .LC0[rip]  # arg 0

  mov rax, 0
  call printf@PLT

  push 0  # num 0
  pop rax
  mov rsp, rbp
  pop rbp
  ret

このコード(asm.sとする)をgccコンパイルして実行するには以下のコマンドでできる。

gcc -o asm asm.s
./asm

実行すると、4byteへのキャストした結果として、0xFFFFFFFFの出力が確認できる。 ちなみに、以下の行をコメントアウトするとキャストなしの結果として、0xFFFFFFFFF(9桁のF)の出力が確認できる。

mov eax, eax