(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

KOZOS移植のためにブートローダー作ったりH8の割り込み周りを試したりといろいろ準備していたが,いよいよKOZOSを移植しよう.

まず移植にあたってだが,とりあえずPowerPCへの移植ソースの最新版をベースにしてみた.

で,移植したのがこれ.とりあえずコマンドプロンプトを出して,コマンド受け付けて動作するようになっている.まあ移植にあたってはいろいろそれなりに苦労したのだけど,詳しい説明は次回に回して,とりあえず公開しよう.ブートローダーにちょこっと手を入れているので,ブートローダーも修正版を公開.移植なのだけど,まずH8が16ビットマイコンなので,intの16ビット対応がいろんなとこに手が入っている.あと割り込み周りをH8に合わせて書き直してある.

まず,OSをビルドしてみよう.ビルド方法は今までと同じ.

hiroaki@teapot:~/h8/os/kozos>% ./make.sh
+ LOCAL=/usr/local
+ TARGET=h8300-elf
+ TARGETDIR=/usr/local/h8300-elf
+ PATH=/usr/local/h8300-elf/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/usr/pkg/sbin:/usr/pkg/bin:/usr/X11R6/sbin:/usr/X11R6/bin:/home/hiroaki/bin: export PATH
+ GCC_EXEC_PREFIX=/usr/local/h8300-elf/bin export GCC_EXEC_PREFIX
+ AR=/usr/local/h8300-elf/bin/ar export AR
+ AS=/usr/local/h8300-elf/bin/as export AS
+ CC=/usr/local/h8300-elf/bin/gcc export CC
+ /usr/local/h8300-elf/bin/gcc -print-prog-name=cpp
+ CPP=cpp export CPP
+ CXX=/usr/local/h8300-elf/bin/gcc export CXX
+ FC=/usr/local/h8300-elf/bin/f77 export FC
+ LD=/usr/local/h8300-elf/bin/ld export LD
+ NM=/usr/local/h8300-elf/bin/nm export NM
+ RANLIB=/usr/local/h8300-elf/bin/ranlib export RANLIB
+ SIZE=/usr/local/h8300-elf/bin/size export SIZE
+ ADDR2LINE=/usr/local/h8300-elf/bin/addr2line export ADDR2LINE
+ GASP=/usr/local/h8300-elf/bin/gasp export GASP
+ OBJCOPY=/usr/local/h8300-elf/bin/objcopy export OBJCOPY
+ OBJDUMP=/usr/local/h8300-elf/bin/objdump export OBJDUMP
+ STRINGS=/usr/local/h8300-elf/bin/strings export STRINGS
+ STRIP=/usr/local/h8300-elf/bin/strip export STRIP
+ HOSTED_CC=/usr/bin/cc export HOSTED_CC
+ NOGCCERROR=yes export NOGCCERROR
+ OBJECT_FMT=ELF export OBJECT_FMT
+ MAKE=gmake -f Makefile
+ export MAKE
+ set -x
+ exec gmake -f Makefile
/usr/local/h8300-elf/bin/gcc -c startup.s -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c main.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c interrupt.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c lib.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c serial.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c thread.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c syscall.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c memory.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c idle.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c extintr.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c kozos.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc -c command.c -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS
/usr/local/h8300-elf/bin/gcc startup.o main.o interrupt.o lib.o serial.o thread.o syscall.o memory.o idle.o extintr.o kozos.o command.o -o kozos -Wall -mh -nostdinc -nostdlib -fno-builtin -I. -Os -DKOZOS -static -T ld.scr -L.
cp kozos kozos.elf
/usr/local/h8300-elf/bin/strip kozos
hiroaki@teapot:~/h8/os/kozos>%

では実行してみよう.ブートローダーも修正してあるので,ブートローダーをフラッシュROMに書き込んで起動する.

teapot# cu -l /dev/cuad0
Connected
kzboot (kozos boot loader) started.
kzboot>

ブートローダーの起動メッセージだけど,まあいままでは開発中の名残でHello World とか出していてそのままでちょっとアレだったので,ついでにちょっと変えてみた.プロンプトもついでに修正.

で,OSをダウンロード.

kzboot> load
~CLocal command? lsx kozos
Sending kozos, 56 blocks: Give your local XMODEM receive command now.
Bytes Sent: 7296 BPS:767

Transfer complete
eceive succeeded.
kzboot>

けっこうサイズがギリギリになってしまっている.

で,OSを起動してみる.

kzboot> run
starting from entry point.
kozos boot succeed!
command>

とりあえず問題なく起動できている.

てきとうにコマンド入力してみよう.

command> echo aaa
aaa
OK
command> threads
extintr
idle
command1
OK
command>

おー,ちゃんと動作している.すげえ.感動.

うーむ,これでひとまずブートローダーから起動して自作OSをダウンロード,起動するとこまでひととおりできたわけだ.まあ実作業はたいした時間はかかってないのだけど,昔むかしに「OS作ってみたい!」とおもっていろいろ勉強してきた成果というか,なんというか,感慨深いなあ.感動だー.

ソースの詳しい説明はとりあえず次回.いやー,動いた動いた.

とりあえず移植して割り込み処理まわりとか必要な部分を書いて、ブートだけはできるようになった。

(ブートローダーにも何箇所か手を入れた)


で、起動はできたのだけど、起動直後に落ちる。まああらかた書いたので、あとはデバッグかな。


(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

連休だ!OS作成日和だ!

今週はいろいろと忙しくてなんだかんだいって移植作業が進んでいなかったので,今日いっきに作業してしまおう.(そして連休は遊ぶのだ)

で,今回なのだけど,とりあえずKOZOS移植の前に準備としてシリアル割り込み処理を実装してみよう.

H8だとシリアル割り込み処理も簡単で,割り込みフラグを立てるだけ.実装したのは以下のような感じ.今回はブートローダーには手は入っていないので,アプリ側の修正のみ.

修正内容は以下のような感じ.まず serial.c の修正.

diff -ruN h8_06/os/serial.c h8_07/os/serial.c
--- h8_06/os/serial.c Tue Sep 15 00:27:07 2009
+++ h8_07/os/serial.c Sat Sep 19 17:24:41 2009
@@ -1,4 +1,5 @@
#include "types.h"
+#include "interrupt.h"
#include "serial.h"

#define H8_3069F_SCI0 ((volatile struct h8_3069f_sci *)0xffffb0)
@@ -42,15 +43,35 @@
#define H8_3069F_SCI_SSR_RDRF (1<<6)
#define H8_3069F_SCI_SSR_TDRE (1<<7)

+void serial_interrupt()
+{
+ volatile struct h8_3069f_sci *sci = H8_3069F_SCI1;
+ int status, c;
+
+ status = sci->ssr;
+
+ c = serial_getc();
+ serial_putc(c);
+
+ sci->ssr = status & ~H8_3069F_SCI_SSR_RDRF;
+
+ return;
+}
+
int serial_init()
{
volatile struct h8_3069f_sci *sci = H8_3069F_SCI1;

+ /* 割り込みハンドラ登録 */
+ interrupt_sethandler(VECTYPE_RXI1, serial_interrupt);
+
sci->scr = 0;
sci->smr = 0;
sci->brr = 64; /* 20MHz 9600bps */
sci->scr = H8_3069F_SCI_SCR_RE | H8_3069F_SCI_SCR_TE;
sci->ssr = 0;
+
+ sci->scr |= H8_3069F_SCI_SCR_RIE; /* 割り込み許可 */

return 0;
}

割り込みハンドラを用意してinterrupt_sethandler()で登録して,あとSCRの割り込みフラグを立てて割り込み有効にしているだけ.ハンドラ内では1文字受信して,そのままエコーバックしている.注意として,1文字読み込みしてから RDRF フラグを落とすこと.(でないと受信できない)

次に main.c の修正.

diff -ruN h8_06/os/main.c h8_07/os/main.c
--- h8_06/os/main.c Tue Sep 15 00:27:07 2009
+++ h8_07/os/main.c Sat Sep 19 17:24:41 2009
@@ -66,6 +66,7 @@
}
#endif

+#if 0
static int gets(char *buf)
{
int i = 0, c = -1;
@@ -78,6 +79,7 @@
}
return i - 1;
}
+#endif

#if 0
static int dump(char *buf, int size)
@@ -105,7 +107,9 @@

int main()
{
+#if 0
char buf[80];
+#endif

init();

@@ -114,10 +118,12 @@
ENABLE_INTR;

while (1) {
+#if 0
puts("os> ");
gets(buf);
puts(buf);
puts("\n");
+#endif
}

return 0;

単に従来のポーリング処理に蓋をしただけ.

では実行してみよう.今回はブートローダーには手は入っていないので,フラッシュROMを書き換える必要は無い.

Hello World!
> load
~CLocal command? lsx hello
Sending hello, 19 blocks: Give your local XMODEM receive command now.
Bytes Sent: 2560 BPS:870

Transfer complete
eceive succeeded.
> run
starting from entry point.boot succeed!
IabIcIABICIIIIIII

問題なくあっさり動いた.H8は楽でいいねー.

これでKOZOS移植の準備はだいたいできた.あとはいよいよOS実装だ!
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

ちょっと別件の作業をしていたので滞っていたけど,昨日ようやくそっちがなんとか片付いたので,H8移植をまた進めよう.

今回は,割り込み処理の実装だ.とりあえずタイマ割り込みを実装したい.

で,いろいろちょっと調べたのだけど,今回利用しているH8ボードでは,割り込みベクタがROM上にあるのでソフトウエアから起動後に書き換えるのは無理っぽい.なので,割り込みベクタの管理はブートローダーにまかせるしかない.ていうかブートローダーのROMへの書き込み時に,割り込みベクタもいっしょに書き込まれてしまい,OS起動後に設定しなおすようなことができない.

で,とりあえず割り込みの入口はブートローダーにやってもらって,ソフトウエア的に独自に割り込みベクタみたいなのをRAM上に作っておいて,ブートローダー側ではそのソフトウエア・割り込みベクタを見て適切なアドレスに飛ぶ,というよくありがちな2段構成にしてみよう.これならソフトウエア・割り込みベクタをOS側で書き換えることで,割り込みハンドラを自由に登録できるようになる.

ソフトウエア・割り込みベクタの場所なのだけど,RAMの先頭領域である0xffbf20 から256バイトを割り当てよう.現状でRAMの先頭付近からOSをロードして使うようになっているので,OSのロード先は256バイト後ろにずらして調整する.

ということで実装したのが以下のような感じ.まずブートローダー側の修正なのだけど,割り込みの入口として intr.S を新規追加してある.ちょっとベタな書き方になってしまっていて,ほんとはマクロとか使って短くしたいところなんだけど,なんかうまくアセンブルできなかったので面倒なのでベタで書いてしまった.

ちなみにファイル名が *.s だとプリプロセッサを通さないが,*.S だとプリプロセッサを通して前処理が行われるようだ(man gcc にそう書いてあった).アセンブラのソースで #define とか使いたいときは要注意.なお intr.S は内部ではひとまず #define とかは使ってないので現状ではプリプロセッサを通す必要はないのだけど,将来的に #define でまとめたいので,intr.S というファイル名にしている.

vector.c では割り込みハンドラとしてとりあえずスタートアップを全部に登録していたが,intr.Sで定義してある各割り込みに応じたハンドラを登録するように修正した.

割り込みの入口では,スタックにレジスタを退避して,割り込みベクタ番号を引数にして interrupt() という関数を呼んでいる.interrupt() の本体はinterrupt.c で定義してあって,ソフトウエア・割り込みベクタ(VECTORS[])を参照して,登録してあるハンドラを呼び出すような作りになっている.OSが起動したらここにハンドラを登録しておけば,割り込みが入ったときに適切にハンドラを呼び出してくれるわけだ.割り込み関連の定義は interrupt.h に書いておいた.

あと main.c からソフトウエア・割り込みベクタの初期化用関数であるinterrupt_init()を呼んで,最初はNULLクリアするようにしている.

ブートローダーの修正はこんなもんかな.次にOS側の修正.

まず,RAMの先頭である 0xffbf20 番地はソフトウエア・割り込みベクタを配置するので,プログラムのロード先を256バイト後ろにずらす.このためにリンカスクリプトに以下の修正を入れている.

--- hello/ld.scr Wed Sep 2 23:32:57 2009
+++ timer/ld.scr Mon Sep 14 22:13:00 2009
@@ -4,8 +4,11 @@

MEMORY
{
- /* reserve 256bytes space for ELF header */
- ram(rwx) : o = 0xffbf20 + 0x100, l = 0x004000 - 0x100 /* 16kb */
+ /*
+ * reserve 256bytes space for software interrupt vector
+ * and 256bytes space for ELF header.
+ */
+ ram(rwx) : o = 0xffbf20 + 0x200, l = 0x004000 - 0x200 /* 16kb */
stack(rw) : o = 0xffff00, l = 0x000010 /* end of RAM */
}

まあ,単にずらしただけ.

あとタイマ関連の処理のために timer.[ch] を追加.これらはH8の持つタイマの各種レジスタを操作して,タイマ起動とあと割り込み時の処理を行うもの.ボード付属のCD-ROMについてたH8のマニュアルとか,あと本とか読んでてきとうに書いてみた.とりあえず,1/50秒周期でタイマをかけてカウントして,1秒おきに「I」という文字を出力するようにしてみた.

あと割り込み処理用にブートローダーの interrupt.[ch] を流用して,ソフトウエア・割り込みベクタに割り込みハンドラを登録できるようにした.これは実際には timer.c のタイマ初期化関数である timer_init() で,

int timer_init()
{
...
interrupt_sethandler(VECTYPE_IMIA0, timer_interrupt);
...

のようにして,timer_interrupt() をベクタ番号24(IMIA0)に登録している.

main.c では timer_init() を呼んでタイマの初期化をして,あと ENABLE_INTR を呼んで割り込み有効にしている.

では,実際に実行してみよう.今回はブートローダーに割り込み処理の機能追加がされているので,まずはブートローダーをビルドしてフラッシュROMに上書きする.

teapot# make write
../../h8write/h8write -3069 -f20 kzload.mot
H8/3069F is ready! 2002/5/20 Yukio Mituiwa.
writing
WARNING:This Line dosen't start with"S".
Address Size seems wrong
WARNING:This Line dosen't start with"S".
Address Size seems wrong
.........................................................................
EEPROM Writing is successed.
teapot#

気のせいか,書き込み時の「.」の数が増えたような...intr.S がベタで書いてあるのが効いているのかしら.

次にブートローダーを起動して,OSをダウンロードして実行する.

teapot# cu -l /dev/cuad0
Connected
Hello World!
> load
~CLocal command? lsx hello
Sending hello, 20 blocks: Give your local XMODEM receive command now.
Bytes Sent: 2688 BPS:665

Transfer complete
eceive succeeded.
> run
starting from entry point.boot succeed!
os> IIItest
test
os> IIIsampleI
sample
os> III

なんと驚くことに一発で動いてしまって,作った本人がビックリ.

いやホントに一発で動いたのよ.今回は割り込み周りのコーディングだし,H8のアセンブラってろくに書いたことないし,もっとハマることを覚悟していたのだけど,コーディングしただけであっさり動いてホント,びっくり.

なんつーか,H8は使うのが簡単でとても楽.H8だとシリアルもタイマもちょろっと設定しただけであっさり動くのですげー楽.PowerPCだとこうはいかないよなあ.

とりあえず割り込みも受けられるようになって,なんか随分前進した感じ.次はシリアルの受信割り込みかなあ.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回作成したブートローダーだけど,前回はちょろっと動作させただけでろくに解説していなかったので,ちょっと解説しよう.

まずブートローダーについてなのだけど,リンカスクリプトに以下の行が追加されている.

buffer(rwx) : o = 0xffdf20, l = 0x002000 /* 8kb */

さらにデータ領域とBSS領域は,上記 buffer 領域に割り当てられるように変更してある.

これはどーいうことかというと,まあ本当はシリアルからプログラムをダウンロードしながらロード先のメモリ上に直接展開できればいいのだけど,面倒なので,いったんRAM上のバッファにぜんぶダウンロードして,で,ELFヘッダを解析してロード先にコピーするという構成にしてある.

で,プログラムをロードする先はRAMの先頭付近にしておきたいので,RAMの終端付近にデータ領域とBSSを置いてブートローダーはそこで動作する,というメモリ配置にしてある.バッファはブートローダー内で

#define BUFSIZE 4096
static unsigned char loadbuf[BUFSIZE];

のようにして文字配列として定義してあるので,BSSに配置される.これが上記の buffer 領域に置かれることになる.loadbufのサイズが4KBなので,buffer 領域は余裕をみて8KBとして定義している.

いったんバッファ上に置いておいて,さらにロード先に展開することを考えると,loadbuf[]の最大サイズは,RAMの16KBの半分の8KBとなる.(loadbuf[]を12KBにすれば12KBまでのプログラムをダウンロードできるが,残りRAMは4KBしかないので展開先が無くて意味がない.よってRAMサイズの半分が,バッファサイズの最大となる.

このためこのブートローダーは現状,8KBまでのプログラムしかロードできない.あとロードされるプログラムは,RAMの先頭付近を使うようにリンカスクリプトでメモリ配置する必要がある.

あと serial.c には,バイナリデータの送受信用に serial_getb(), serial_putb() を新規作成した.これらは改行コードの変換を行わずに生データを送受信する.前回はここで従来の serial_getc(), serial_putc() を使っていたために改行コードが変換されてしまってチェックサムとかが壊れてしまう,というのにハマッたわけだ.

あと uuencode 形式のデコードのために,uudecode.c を追加してある.xmodem.c からは USE_UUENCODE の定義の有無で,uudecode を呼び出すか,生データとして扱うかが切り替わるようになっている.シリアルからデータをダウンロードしながらデコードを行えるように,uudecode.c はストリームデータを処理できるような書き方をしてある.(同様に書けば,ELF形式をダウンロードしながらヘッダ解析してロード先に直接展開,ということもできると思う)

あとELF形式の解析のために elf.c が追加されている.ELF形式はそれなりに複雑なフォーマットだけど,ここでやってるのはELFヘッダのチェックをしてからプログラムヘッダを見て展開することだけなので,超簡単に書けている.ELF形式について詳しくはこの記事の第2回を参照してほしい.

main.c は,putval(), putxval() に実は負の値を渡すと無限ループに陥ってしまうという問題があったので,引数をunsignedとして受け取るように修正.あと run コマンドを追加して,ダウンロードしたプログラムをロード先に展開して実行するように機能追加してある.

次に,ダウンロードされるプログラムのほうについて.まあ現状では入力された単語をエコーするだけの機能しかなくて,実はブートローダーを流用して書いてある.

シリアルまわりとかスタートアップはブートローダーから持ってきたそのまま.スタートアップ(startup.s)ではスタックポインタを設定しているので,サンプルプログラムを起動するとスタックは設定されなおすことになる(ブートローダーが使用していたスタックを引続き利用するわけではない).ライブラリ(lib.c)もそのまま.異なるのはリンカスクリプトとmain.cなのだけど,main.cは使わない関数に蓋をして,単語をエコーするだけに書き換えただけなのでたいした変更は無い.あ,あとデータ領域の展開とBSSのクリアはブートローダー側でやっているので,main.c の init() からは削除してある.

大きな変更があるのはリンカスクリプトだ.以下,ブートローダーとロードされるサンプルプログラムでの,リンカスクリプトの変更点.

--- kzload/ld.scr Fri Sep 4 00:16:17 2009
+++ os/ld.scr Fri Sep 4 00:16:35 2009
@@ -4,44 +4,38 @@

MEMORY
{
- vectors(r) : o = 0x000000, l = 0x000100 /* top of ROM */
- rom(rx) : o = 0x000100, l = 0x07ff00 /* 512kb */
- ram(rwx) : o = 0xffbf20, l = 0x004000 /* 16kb */
- buffer(rwx) : o = 0xffdf20, l = 0x002000 /* 8kb */
+ /* reserve 256bytes space for ELF header */
+ ram(rwx) : o = 0xffbf20 + 0x100, l = 0x004000 - 0x100 /* 16kb */
stack(rw) : o = 0xffff00, l = 0x000010 /* end of RAM */
}

SECTIONS
{
- .vectors : {
- vector.o(.data)
- } > vectors
-
.text : {
_text_start = . ;
*(.text)
_etext = . ;
- } > rom
+ } > ram

.rodata : {
_rodata_start = . ;
*(.strings)
*(.rodata)
_erodata = . ;
- } > rom
+ } > ram

- .data : AT(_erodata) {
+ .data : {
_data_start = . ;
*(.data)
_edata = . ;
- } > buffer
+ } > ram

.bss : {
_bss_start = . ;
*(.bss)
*(COMMON)
_ebss = . ;
- } > buffer
+ } > ram

_end = . ;



まずロード時ていうかリンク時の注意なのだけど,ELF形式はリンカによって作成される
際に,先頭にELFヘッダが付加される.で,このELFヘッダがテキスト領域の直前に
配置&ロードされるようにプログラムヘッダが作成される.




これはロードされたプログラム側で,自身のELFヘッダを参照できるようにするためだと
思われる(あくまで想像だけど,もしかしたら共有ライブラリがらみで参照が必要
なのかもしれない)のだが,RAMの先頭(0xffbf20)から利用するようにふつうに書くと,
RAM領域の先頭として指定された 0xffbf20 というアドレスよりも少し前にELFヘッダが
はみ出して配置されてしまう.いろいろやってみたのだけどこれをうまく削ったり
調整したりする方法が見つからなくて,しかたがないので対策として,RAM領域の
先頭256バイトをELFヘッダ用に空けるようにしてある.
(今思ったのだけど,ブートローダがプログラムヘッダを見て展開するときに,
RAM領域内でないデータは展開しないようにすればいいだけだね.まあでもいいや)




ということで,サンプルプログラムのほうではRAMは先頭 0xffbf20 付近から利用する.
RAMの終端のほうはブートローダーが利用しているので,ブートローダーからロードした
プログラムに実行が移るまでは,上書きすることはできない.つまり,RAM終端付近に
なにかを配置するようにリンカスクリプトを書いてはいけない.




割り込みベクタはいまのところとくに上書きしていないので,リンカスクリプトから
削除.あとテキスト,リードオンリーデータ,データ,BSSをすべてRAM上に配置する
ように変更してある.




まあ解説はこんなとこかな.ざざっと説明しただけだけど.