google-site-verification: google3bd66dd162ef54c7.html

ArduinoのCPUのデーターを全部抜く(ダンプする)

 最近テレビで「池の水ぜんぶ抜く」、という番組をよくやってます。それに少し影響を受けて、Arduino の CPU の ATmega328P に入っている情報を全部抜くプログラムを作ってみました。まあ、抜くと言ってもデーターを、メモリーマップの形でシリアルにダンプするだけのものです。なお、この記事はArduino UNO で動作確認を行っています。

 普通のダンププログラムは、調べたい相手の状態を出来るだけ変えないで情報を出力します。それに対して、今回作ったプログラムは、ダンプ用のプログラムを書き込んで、その状態を出力しています。つまり、まるっきり相手を乗っ取った状態になっているので、あらためて調べなくても、ほとんどは事前に予想出来る情報のはずです。ということで、ちょっとインチキ臭い話です。

 そうは言っても、ダンプ出力を完全に予想するためには、システム全体に対する深い知識が必要ですが、簡単なことではありません。そういう意味では、このプログラムの存在意義はあると思います。

 ちなみに、CPU の内部構造の説明にメモリーマップがよく出てきますが、実データーが入った状態の解説はお目にかかったことがありません。本物のデーターを見ながら勉強すれば、より具体的にCPUを理解できるはずです。

 そんなことを考えて、CPU の情報を全部抜く(ダンプする)プログラムを作ってみました。

 先に実行した結果を示します。

▼CPUとダンプしたデータ
ダンプリストとCPUチップ
 これは、ダンプしたデータをプリントしたものです。小さな CPU の中に、A4 で14ページものデーターが入っているのは驚くばかりです。(両面印刷しています)

 以下、まずはプログラムです。
/* CPUの全レジスタとメモリーをダンプ
対象範囲(ATmega328P)
・IOメモリ領域:0x00-0x8FF(レジスタ、IOレジスタ、拡張IOレジスタ、RAM)
  8ビットx(2048+256) = 2304バイト
・EEPROM:0x00-0x1FF
 8ビットx512 = 512 バイト
・Flash Memory:0x00-0x3FFF
 16ビット×16384ワード = 32768バイト(32k)
2018/4/3 ラジオペンチ http://radiopench.blog96.fc2.com/
*/

#include <EEPROM.h>

char ascii[32]; // 文字列記録バッファ

void setup() {
Serial.begin(115200);
Serial.println("ATmega328P registers & memorys dump");
Serial.println();

Serial.println("REGISTER FILES"); // CPUレジスタ
IoDump(0, 0x001F);
Serial.println("I/O REGISTERS"); // IOレジスタ
IoDump(0x0020, 0x005F);
Serial.println("EXTENDED I/O REGISTERS"); // 拡張IOレジスタ
IoDump(0x0060, 0x00FF);
Serial.println("SRAM DATA MEMORY"); // SRAM
IoDump(0x100, 0x08FF);
Serial.println("EEPROM"); // EEPROM
eepromDump(0, 0x1FF);
Serial.println("FLASH PROGRAM MEMORY (2byte/word,Little Endian)"); // フラッシュメモリ
flashMemDump(0x0000, 0x3FFF);
}
void loop() {
}

void IoDump(unsigned int fromAdr, unsigned int toAdr) { // メモリ領域ダンプ
int p = 0;
unsigned int i;
byte data;
Serial.println("addr 0 1 2 3 4 5 6 7 8 9 A B C D E F <- ascii char ->");

for (i = fromAdr; i <= toAdr; i++) { // データーメモリ領域
if (( i & 0x0F) == 0) { // 行の先頭なら
printHex4chr(i); // アドレス表示
Serial.print(":");
clrAscii(15); // バッファを16文字分クリア
p = 0;
}
Serial.print(" "); // データ間にスペース入れる
data = _SFR_MEM8(i); // データメモリ読み出し
printHex2chr(data); // 16進2文字で表示
addAscii(data, p); // 文字コードなら記録
p++;

if ((i & 0x0F) == 0x0F) { // 行末だったら、
Serial.print(" ");
printAscii(15); // 該当文字を表示
Serial.println(); // 行末なので改行
}
if ((i & 0xFF) == 0xFF) { // 0x100間隔で1行開ける
Serial.println();
}
}
Serial.println();
}

void flashMemDump(unsigned int fromAdr, unsigned int toAdr) { // フラッシュメモリダンプ
int p = 0;
unsigned int i;
unsigned int data, dataH, dataL;
Serial.println("addr 0 1 2 3 4 5 6 7 8 9 A B C D E F <--------- ascii char --------->");

for (i = fromAdr; i <= toAdr; i++) { // 指定範囲をスキャン
if (( i & 0x0F) == 0) {
printHex4chr(i); // アドレス表示
Serial.print(":");
clrAscii(31); // 32文字分クリア
p = 0;
}
Serial.print(" "); // データ間にスペース入れる
dataL = pgm_read_byte(i * 2); // 1バイト目を下位
dataH = pgm_read_byte((i * 2) + 1); // 2バイト目を上位に(リトルエンディアンで格納)
data = (dataH << 8) + dataL; // 16ビットに結合

printHex4chr(data); // 16進4字で表示
addAscii(dataH, p); // 文字コードなら記録
p++;
addAscii(dataL, p); // 文字コードなら記録
p++;

if ((i & 0x0F) == 0x0F) { // 行末だったら、
Serial.print(" ");
printAscii(31); // 該当文字列を表示
Serial.println(); // 行末改行
}
if ((i & 0xFF) == 0xFF) { // 0x100間隔で1行開ける
Serial.println();
}
}
Serial.println();
}

void eepromDump(unsigned int fromAdr, unsigned int toAdr) { // EEPROMのダンプ
int p = 0;
unsigned int i;
unsigned int data;
Serial.println("addr 0 1 2 3 4 5 6 7 8 9 A B C D E F <- ascii char ->");

for (i = fromAdr; i <= toAdr; i++) { // 指定範囲をスキャンん
if (( i & 0x0F) == 0) { // 行の先頭だったら
printHex4chr(i); // アドレス表示
Serial.print(":");
clrAscii(15);
p = 0;
}
Serial.print(" "); // データ間にスペース入れる
data = EEPROM.read(i); // EEPROM 読み出し

printHex2chr(data); // 16進2文字で表示
addAscii(data, p); // 文字コードなら記録
p++;

if ((i & 0x0F) == 0x0F) { // 行末だったら、
Serial.print(" ");
printAscii(15); // 該当文字を表示
Serial.println(); // 行末を改行
}
if ((i & 0xFF) == 0xFF) { // 0x100間隔で1行開ける
Serial.println();
}
}
Serial.println();
}

void clrAscii(int n) { // 文字バッファを指定数クリア
for (int x = 0; x <= n; x++) {
ascii[x] = ' ';
}
}

void addAscii(byte d, int p) { // 文字コードだったら指定位置に記録
if ((d > 0x20) & (d < 0x7F)) { // キャラクタコードの範囲だったら
ascii[p] = d; // バッファの指定位置に記録
}
}

void printAscii(int n) { // 文字バッファの内容を指定数表示
for (int x = 0; x <= n; x++) {
Serial.print(ascii[x]); // バッファの文字を表示
}
}

void printHex2chr(byte x) { // 16進2桁で出力
Serial.print((x >> 4), HEX); // 上位
Serial.print((x & 0x0F), HEX); // 下位
}

void printHex4chr(unsigned int x) { // 16進4桁で出力
// x &= 0xFFFF;
Serial.print((x >> 12), HEX); // 最上位
Serial.print(((x & 0x0F00) >> 8), HEX);
Serial.print(((x & 0x00F0) >> 4), HEX);
Serial.print((x & 0x000F) , HEX); // 最下位
}
 
 吐き出す情報量が多いのでシリアルのビットレートを 115200bps と高くしています。あとは、細かい説明は省略しますが、下記がCPUからデーターを読み出している部分です。
・51行目の、
   data = _SFR_MEM8(i); // データメモリ読み出し
・82,83行目の
  dataL = pgm_read_byte(i * 2); // 1バイト目を下位
  dataH = pgm_read_byte((i * 2) + 1);
・118行目の
  data = EEPROM.read(i); // EEPROM 読み出し

 以下ダンプ出力を先頭から順に見て行きます。最初はデーターメモリー領域です。

▼CPU レジスタ、IO レジスタ、拡張 IO レジスタ
出力例1
 ここには、CPU レジスタ、IO レジスタ、拡張 IO レジスタの順に並んで始まります。

 0x00から0x1Fまでは CPU レジスタです。ここの内容は激しく変化しているので、ダンプ結果はある瞬間を捉えたものにすぎず、内容にほとんど意味は無いと思います。でもデーターメモリ領域の最初の32バイトが、CPU レジスタに割り当てられていることは良く判ると思います。

 0x20から0x5Fの範囲は IO レジスタで、ここでデジタル IO ポートなどの基本的な IO の制御を行っています。ちなみに、この領域はアセンブラの IO 命令からアクセス可能で、その場合のアドレスは0x00から始まります。なお、レジスタが存在しないアドレスもあり、その場所の値は不定になりますが、このダンプでは0xB8となっています。

 0x60から0xFFまでの領域は拡張 IO レジスタで、ここは ADコンバーターや I2C などの設定を行う領域です。

 IO レジスタはタイマーやADコンバーター、通信機能をコントロールする部分です。ここをいじっているだけで、相当遊んでいられると思います。

▼SRAM
出力例2

 0x100から0x8FFまでの2kバイトがSRAM領域で、プログラムの変数やスタックなどの保存に使われています。

 右のアスキーキャラクタ欄を見ると、プログラムで入力した文字列がそのまま入っている部分があります。これらは固定されたデーターなので途中で変更されることはありません。それにもかかわらずRAM領域に置かれているのはもったいないです。

 ここまでがデーターメモリ領域で、次はEEPROM領域です。

▼EEPROM
出力例3

 EEPROM領域は0x000から0x1FFまでの512バイトです。
 過去に EEPROM に書き込みを行ったことがあれば、その内容が残っています。まっさらな CPU ならデーターは全部 0xFF になっているはずです。

 次はフラッシュメモリ領域です。この領域は32kバイトの容量がありますが、1ワードが2バイトの構成になっているので、ワードアドレスとしては16kまでの範囲です。

▼フラッシュ プログラム メモリー領域
出力例4

 アドレスは0x0000から0x3FFFまでの16kワードの範囲です。上の画像は先頭部分で、データーはこのまま10ページ以上続きます。

 なお、この領域をバイト単位で表現することもあるようで、上記のプログラムの pgm_read_byte() 関数はバイトアドレスでアクセスしています。でも話がややこしくなるので、ここではワード単位の表現に変換しています。

 2バイトで1ワードを構成する時のバイトオーダーは、AVR の作法に合わせて、リトルエンディアンにしています。こうするとマシン語が読み易くなります。一方でアスキー文字が読み難くなりますが、まあこれは仕方ないでしょう。

 ダンプリストを見ると、先頭アドレス付近にリセットや割込み処理へのジャンプルーチンがまとめて置かれていることがよく判ります。ちなみに、先頭の、0x940C, 0x0035 というのは、リセットがかかったら0x0035番地へジャンプというマシン語だと思います。

 フラッシュメモリの領域は、過去に書かれたプログラムの痕跡ががそのまま残っているため、どこまでが今回書き込んだプログラムの範囲なのかは、はっきりとは判りません。

 フラッシュメモリ領域の末尾にブートローダーが書かれているはずなのですが、残念ながら今回作ったプログラムでは内容を見ることが出来ませんでした。たぶん、何かのおまじないが必要なんでしょう。

◆まとめ
 ダンプ結果を見ると、CPU の内部構造が良く判ります。マイコンのアーキの勉強をやりたい人には、良い教材になると思います。なお、このプログラムは Arduino の IDE と UNOが動いていれば他に何も無くても動きます。

 実は最初は、IO レジスタの範囲だけをダンプするプログラムを作ろうと持っていました。でも、「池の水ぜんぶ抜く」のコピーが気に入ったので、全メモリー領域の情報を抜くことにしました。

 実はヒューズビットとシグネチャの情報もダンプしたかったのですが、今回はそこまで手が回りませんでした。ただ、例えば Arduino UNO で動いているということは、ヒューズやシグネチャの値ははぼ決まるはず、というか変な値になっていたら Arduino のIDE が動かないはずなので、これらを詳しくチェックしても、あまり意味が無いかも知れません。

 フラッシュの内容を逆アセンブルしてアセンブラに戻すと面白いかも知れません。でもそんなことはHEXファイルから遡った方が手っ取り早いですね。
関連記事

コメントの投稿

管理者にだけ表示を許可する

ダンプリスト、懐かしい!

TK80やPC8001の時代を思い出します。
あの頃はテープレコーダーで保存していたので大変でした。
PC8801を購入したころにはセカンドメーカー製DISKドライブが何とか手に入る価格に下がりましたが…

今はいろいろ便利過ぎる時代になりましたね。

れ:ダンプリスト、懐かしい!

mytoshiさん、今晩は。

そうですね。昔のマイコン雑誌にはこんなダンプリストがいっぱい掲載されていました。

No title

私も、8080やZ80の頃のマシン語を扱っていた頃を思い出しました。
全体像を理解するのにダンプリストが必要でしたね。
また、ダンプリストを出力するツール作成も、ハード&ソフトの導入教材でした。

きたきたさん、おはようございます

ダンプリストは、ハードを理解するために役立ちますよね。

私の会社はモトローラー系のマイコンが多かったので、MC6800やMC6809のアセンブラをよくいじってました。
カレンダー
03 | 2018/04 | 05
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 - - - - -
プロフィール

ラジオペンチ

Author:ラジオペンチ
電子工作を中心としたブログです。たまに近所(東京都稲城市)の話題など。60過ぎて視力や器用さの衰えを感じつつ日々挑戦!
コメントを入れる時にメールアドレスの記入は不要です。なお、非公開コメントは受け付けていません。

記事が気に入ったらクリックを!
最新記事
カテゴリ
最新コメント
リンク
FC2カウンター
検索フォーム
月別アーカイブ
RSSリンクの表示
QRコード
QRコード