我们在编写裸机程序(baremetal
)、虚拟化管理程序(hypervisor
)和操作系统(OS
)时,Debug
分析程序是必不可少的。不像linux内核,有大量的调试方法,很多裸机程序、hypervisor
没有完善的调试分析方法。
异常相关寄存器
但也不是无计可施,在硬件上,ARM架构为程序的异常行为提供了详细的寄存器:
ESR_ELx
寄存器(x=1,2,3
)
保存发生异常时的特征,比如异常分类(ESR_ELx.EC
)、异常具体原因(ESR_ELx.ISS
)等。ELR_ELx
寄存器(x=1,2,3
)
保存发生异常时,保存要返回的地址,一般情况下就是发生异常时的指令地址。FAR_ELx
寄存器(x=1,2,3
)
保存错误地址。HPFAR_EL2
寄存器
保存stage-2
阶段的地址转换发生的错误IPA地址。
ARM从硬件架构上设计了4层异常级:EL0
、EL1
、EL2
和EL3
。不同特权等级的程序,运行在不同的异常级上。本文从hypervisor
虚拟机管理程序的角度,讲解如何利用这些寄存器,对程序的异常情况进行分析。
hypervisor本身的abort异常
我们以meta-hypervisor
出现的具体异常为例:
esr_el2 = 0x97010046 elr_el2 = 0xfd8000005880 far_el2 = 0xfd8000005880
在这儿,esr_el2
的值为0x97010046
,对应的位域为:
EC |
IL |
ISS |
100101 |
1 |
1_0000_0001_0000_0000_0100_0110 |
EC = 100101
:
说明是数据abort异常,但是没有发生异常级改变(EL2)
;或者,当支持嵌套虚拟化时与VNCR_EL2相关的访问产生的数据abort异常
。ISS
编码(数据abort异常的具体原因)
ISV |
SAS |
SSE |
SRT |
SF |
AR |
VNCR |
LST |
FnV |
EA |
CM |
S1PTW |
WnR |
DFSC |
24 |
23-22 |
21 |
20-16 |
15 |
14 |
13 |
12-11 |
10 |
9 |
8 |
7 |
6 |
5-0 |
1 |
00 |
0 |
00001 |
0 |
0 |
0 |
00 |
0 |
0 |
0 |
0 |
1 |
000110 |
- 通过上面各个位域的信息,综合得出:
把W1寄存器中一个字节的数据写入内存时发生的错误
。
我们再来看汇编代码中,0xfd8000005880
地址处的内容:
void *memset(void *dest, uint32_t c, uint32_t count) { // ......省略 *d = c; fd8000005874: b94007e0 ldr w0, [sp, #4] fd8000005878: 12001c01 and w1, w0, #0xff fd800000587c: f9400fe0 ldr x0, [sp, #24] fd8000005880: 39000001 strb w1, [x0] d++; fd8000005884: f9400fe0 ldr x0, [sp, #24] fd8000005888: 91000400 add x0, x0, #0x1 fd800000588c: f9000fe0 str x0, [sp, #24] }
- 代码中,
fd8000005880: 39000001 strb w1, [x0]
确实是往x0
寄存器中的地址写入一个字节。这正好与我们对异常原因分析的结果相同。说明异常正是memset
函数发生的错误。
ISV
:1
, 说明23-14位保存着合法指令的异常信息SAS
:00
, 说明访问字节数据时产生的错误SSE
:0
, 字节访问不要求符号扩展SRT
:00001
,错误指令的Wt/Xt/Rt
操作数的寄存器编号SF
:0
, 指令访问的是32位通用寄存器AR
:0
, 指令没有aquire/release语义VNCR
:0
, 保留LST
:00
, 产生abort异常的指令未指定FnV
:0
, FAR寄存器是合法的EA
:0
, 表示不是外部abortCM
:0
, 表示错误不是由cache维护指令产生的S1PTW
:0
, 表示不是stage-2错误WnR
:0
, 表示写内存DFSC
:000110
,L2地址翻译错误
如果memset
函数只有一处调用的话,Bug
原因结合代码就很容易分析出来了。但是,我们自己编写的hypervisor
中有很多处调用memset
函数的地方。所以,就文中的bug
示例而言,目前还不能分析出原因。所以,我们需要使用qemu
模拟器,通过gdb
进行单步调试,看看出问题的代码位置(参见下一篇《QEMU+GDB调试ARM程序》)。
Guest OS的abort异常
我们设计的hypervisor
支持Guest OS
触发的4类异常,具体定义如下:
abort_handler_t abort_handlers[64] = { [ESR_EC_DALEL] = aborts_data_lower, [ESR_EC_SMC64] = smc64_handler, [ESR_EC_SYSRG] = sysreg_handler, [ESR_EC_HVC64] = hvc64_handler };
ESR_EC_HVC64 = 0x16
:用于处理Guest OS
发起的HVC
调用,我们设计使用HVC
指令在VM
之间建立通信。ESR_EC_SMC64 = 0x17
:用于处理Guest OS
发起的SMC
调用,我们知道ARM规定了PSCI规范,通过将电源管理等代码在ATF代码中实现,这样就实现了资源的安全管理。PSCI规范的底层就是通过SMC
指令实现的。hypervisor
需要将Guest OS
发起的虚拟核的PSCI调用转发给物理核。ESR_EC_SYSRG = 0x18
:模拟寄存器和外设。因为Guest OS
需要访问一些特殊寄存器和外设,而外设有时候需要多个VM
共享,hypervisor
对其进行模拟。ESR_EC_DALEL = 0x24
:用于处理Guest OS
发生的abort
异常。比如,Guest OS
访问我们未指定的物理内存。
对于ESR_EL2
寄存器的分析,与前面的一段一样,不在具体详述。
而HPFAR_EL2
寄存器保存着出错的IPA
地址,通过该地址,我们就可以知道,Guest OS
访问哪块内存出错,就能解决某些bug
了。