データフォルトと格闘した記録その1
背景・前提
LSIの検証プログラムで解決困難なエラーが発生したため、ある程度一般的な内容と思われるので記録と解析のモチベーションにするため記事にしてみた。
元々プログラムは動作していて、そこからある変更をしたらCPUが例外を吐いて止まるようになってしまった。
CPUはARM、言語はC。マルチコア環境だが、プログラムそのものはシングルコア用に設計している。
エラー出力とその内容
例外発生時のメッセージは以下。このエラーメッセージは例外発生時にSDKのユーザコードから出力されている。
ESR_EL3 = 0x0000000096000004
FAR_EL3 = 0x0400040004000400
まずは、このエラーが何を言っているのかを調べてみる。
ESR_EL3: Error Syndrome Register
下位32bitは0x9600_0004なので上記のドキュメントによると、
bits | name | value | discription |
---|---|---|---|
[31:26] | Exception Class | 6'b10_0101 | Data Abort taken without a change in Exception level. Used for MMU faults generated by data accesses, alignment faults other than those caused by Stack Pointer misalignment, and synchronous External aborts, including synchronous parity or ECC errors. Not used for debug related exceptions. |
[25] | Instruction Length | 1'b1 | 32bit |
[24:0] | ISS | 25'b0_0000_0000_0000_0000_0000_0100 | ... |
[24:0]ISSのエンコーディングは[31:26]Exception Classによって変わるらしく、今回のException ClassはData Abortなので、以下のエンコーディングの様だ。 ちなみにISSはInstruction Specific Syndromeの略称らしい。
bits | name | value | discription |
---|---|---|---|
[24] | Instruction syndrome valid | 1'b1 | [23:14] is invalid |
[23:14] | ... | 10'b00_0000_0000 | reserved |
[13] | VNCR | 1'b0 | reserved? |
[12:11] | Synchronous Error Type | 2'b0 | reserved? |
[10] | FAR not Valid | 1'b0 | reserved |
[9] | External Abort | 1'b0 | not external |
[8] | Chache Maintenance | 1'b0 | Neither Cache Maintenance Instruction nor Address Translation Instruction |
[7] | S1PTW | 1'b0 | ... |
[6] | Write not Read | 1'b0 | Read? |
[5:0] | Data Fault Status Code | 6'b00_0100 | Translation fault, level 0 |
今回、[24]ISVが0なので、[23:14]はReserved扱い。[13]VNCRはARMv8.4-NVを使っていない限り、[12:11]SETはRAS Extentionを使っていない限り、どちらもReserved扱いになるようだ。多分、今回の環境でも無視してOK…だと思う。[10]FnVは[5:0]DFSCが6'b01_0000の時に意味を持つので、今回はReserved。[6]WnRが0'b0なのでReadアクセスが問題。DFSC[5:0]の”Translation fault, level 0”に着目しようと思う。
ESR_EL3の詳細については、以下のページを参考にした。
FAR_EL3: Fault Address Register
ESR_EL3[31:26]ECの内容で意味合いが変わるが、今回のEC=6'b10_0101 Data Abortの場合、例外発生要因となったアクセス命令がアクセスしようとした仮想アドレスを指すとのこと。今回、仮想アドレスの0x0400_0400_0400_0400番地にアクセスしようとして例外が引き起こされている…ということになるのだろうか?そもそもこの値はアドレスというより普通にデータに見える。
FAR_EL3の詳細は、以下のページから。
結局のところ
仮想アドレス0x0400_0400_0400_0400にReadアクセスしようとしたが、MMUで変換エラーが引き起こされているよ、ってことになるのだろう。
問題の命令の特定
運よく、良い解析ツールを使える環境にあるので、各種トレースから問題の関数と命令の特定を行った。
関数トレース結果
vprint的な関数からcurr_el_spx_sync_defaultを経て、エラー出力関数に飛んでいる。また、例外発生前にも何度かvprint的な関数はコールされているので、この関数が常に悪さしているわけではない。
レジスタトレース結果
一回目のトライアルからvprint的な関数からcurr_el_spx_sync_defaultに飛ぶ瞬間のPCが0x2afcであることがわかった。0x2afcのディスアセンブリは、
2afc: 39400361 ldrb w1, [x27]
ここで汎用レジスタ名がw1になっているが、これはx1に32bitアクセスをするよという意味らしい。まだ64bitアーキテクチャに慣れない。で、命令の意味はx27番地のデータを8bit持ってきて、32bit拡張してからx1にロードせよ。
vprintf的な関数を無事に抜けるときと例外で落ちるときで関連するコアレジスタをみると、以下の差分があった。
register | value(NG case) | value(OK case) |
---|---|---|
PC | 0x2afc | 0x2afc |
w1(x1) | 0xffff_ffd0 | 0xffff_ffd0 |
x27 | 0x0400_0400_0400_0400 | 0x9_d110 |
たしかに、これではx27が指す0x0400_0400_0400_0400番地にReadに行ってしまう。
では、x27にこの値を書いてる不届きな命令は何かというと、その直前だった。
2af8: f940001b ldr x27, [x0]
この命令が実行されるとx27に0x0400_0400_0400_0400がロードされて、次のサイクルで例外で落ちる、という動きの様だ。
この命令を実行時の状況は、以下の内容であった。
register | value(NG case) | value(OK case) |
---|---|---|
PC | 0x2af8 | 0x2af8 |
x0 | 0x821_c268 | 0x821_c728 |
x27 | 0xa12b1 | 0x9_d059 |
結局のところ、
- 0x2fa8の命令でx27レジスタに0x821_c268番地のデータ(0x0400_0400_0400_0400)がloadされて、
- 0x2facの命令でx1レジスタに0x0400_0400_0400_0400番地のデータをloadしようとアクセスに行って落ちる
ということになる。
少しプログラムを変更したりしたところ、どうも直接データアボートを引き起こす命令が0x2afcであることは同じなのだが、呼び出し元が変わるケースがある。タイミングに依存した何かがあるらしい。別のトライアルではx0=0x821_c478だったりした。いずれにせよ、この0x821_c268とか0x821_c478あたりが何なのかを確認する必要がある。まあ、スタック破壊なんだろうなと予想。
つづく。