一、中断向量表介绍
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
以上是中断在百度百科中些较为浅显的解释,中断实际上是操作系统中较为重要的一种概念及机制,关于中断的一些详细的说明可以在由张银奎老师主讲的《在调试器下理解计算系统》中观看到(视频在nano code中;下载链接:Nano Code下载)。
上面提到“转入处理新情况的程序”,那么中断后,该如何确定这个新程序呢?这时候就需要系统的中断向量表了,中断描述符表中存储了中断的相关信息,当CPU被中断后,CPU会获取中断向量,然后查阅这张表,找到对应的中断信息,然后根据中断信息跳去相应的位置处理事情。
今天我们就来详细研究一下ARM框架下的中断向量表。并编写代码把向量表以友好的方式解析出来。
二、查看中断向量表
那么该如何查看中断向量表呢?在ARM框架下系统寄存器VBAR_EL1(Vector Base Address Register)指向的就是中断向量表。
1 . 在Nano Code中获取VBAR_EL1寄存器的地址。
rdmsr VBAR_EL1
2 . 解析这个地址,获取该地址处指令和函数。
ln ffffff8008081800
3 . 此时,我们可以在代码中根据这个函数找到它的地址,编写跨内核版本的代码。
ULONG64 VBAR_EL1_ADDR; // 获取 lk!vectors 函数的地址,并把地址存储到 变量 VBAR_EL1_ADDR 中 GetExpressionEx("lk!vectors", &VBAR_EL1_ADDR, NULL);
三、解析中断向量表
参考和引用
寄存器VBAR_EL1指向的是中断向量表。其中不同向量表间的偏移量、中断类型、中断级别可以参考下图。
(reference:Anatomy of Linux system call in ARM64 | East River Village)
Offset from VBAR_EL1 |
Exception type |
Exception set level |
+0x000 |
Synchronous |
Current EL with SP0 |
+0x080 |
IRQ/vIRQ |
“ |
+0x100 |
FIQ/vFIQ |
“ |
+0x180 |
SError/vSError |
“ |
+0x200 |
Synchronous |
Current EL with SPx |
+0x280 |
IRQ/vIRQ |
“ |
+0x300 |
FIQ/vFIQ |
“ |
+0x380 |
SError/vSError |
“ |
+0x400 |
Synchronous |
Lower EL using ARM64 |
+0x480 |
IRQ/vIRQ |
“ |
+0x500 |
FIQ/vFIQ |
“ |
+0x580 |
SError/vSError |
“ |
+0x600 |
Synchronous |
Lower EL with ARM32 |
+0x680 |
IRQ/vIRQ |
“ |
+0x700 |
FIQ/vFIQ |
“ |
+0x780 |
SError/vSError |
“ |
稍加分析,可以看到,总共有16张中断向量表,其中:
- 每个表都占用128字节(十六进制0x80就是十进制128),
- 每张向量表最多存放32条大小为4字节的汇编指令,
- 每张向量表的起始地址都是VBAR_EL1 + n * 0x80 (n的取值范围 [0, 15])。
再对照下图内核源码:可以看到源码和上图表格中的分析是一一对应的。
ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
END(vectors)
开始解析
1、第一层解析:解析第一层向量表。
向量表之所以称之为表,说明他们内部含有其他数据,数据排列像表格一样。这里我们把sync看作第一层向量表,并对表sync进行解析。
a . 查看向量表的详情信息,根据偏移量0x200确定某个表的的起始地址,表sync的起始地址为VBAR_EL1+0x200,也就是 ffffff8008081800+0x200 。
b . 使用命令u来逐页查看该地址处的汇编指令。
u uffffff8008081800+0x200
再次输入u,会按照每页8条指令继续解析。
2、在表sync中寻找第二层向量表。
在表sync中所有汇编指令中,我们要找的是与函数跳转挂钩的指令,例如 b 或者 b.eq 或者 bl,解析b 的参数,解析出的函数名就是我们寻找的第二层向量表。
这里以解析黄圈内的地址为例
// 根据地址来获取函数名 ln 0xffffff8008082ac0 // 解析地址处的汇编指令 u 0xffffff8008082ac0
此时我们了解到 地址 0xffffff8008082ac0处存放的是 el1_sync函数,该函数就是我们寻找的第二层向量表。
3、第二层解析:对照Linux内核源码,按照内核的逻辑解析第二层向量表el1_sync。
a . 在内核源码中查找该函数,查看其结构和逻辑。
b . 使用指令u在Nano Code中找到与源码对应的部分。
在解析向量表时,我们要显示出内核原本的逻辑和结构,例如解析 el1_sync函数时可以按照以下格式输出:
0x25: lk!el1_da 0x21: lk!el1_ia
按照这种输出格式,依次解析剩余的部分。
c . 到这里我们已经解析了三层了,当然还可以继续追根溯源,但是对于错误向量表的研究,这三层已经足够了。
4、根据以上研究,我们可以把向量表看作树形结构,并按照以下格式来输出。
5 . 每张向量表的处理方式都相似,我们按照上述方法依次处理16张总的向量表。
四、开发调试环境
- 软件:NanoCode调试软件 (Website:Nano Code下载)
- 设备:GDK8套件(Website:GDK8) 挥码枪NTP(Website:NTP)
- 关于NanoCode和GDK8的搭配使用,可以参考博客(GDK8——强大的Linux内核调试工具)
- 代码中相关函数的使用可以参考微软官方:调试器概述 - Windows drivers | Microsoft Learn
五、牵扯到的函数
在NanoCode中可以使用 u、ln、x等命令可以很方便的解析相关信息,在代码中也有特定的函数与其对应。
函数的更多信息请参考微软官方:调试器概述 - Windows drivers | Microsoft Learn
下面是一些简单的用法:
typedef unsigned long long ULONG64; void test(void) { // GetExpressionEx :传入函数名称,获取该函数的地址。对应着NanoCode命令 x ULONG64 VBAR_EL1_ADDR = 0; GetExpressionEx("lk!vectors", &VBAR_EL1_ADDR, NULL); dprintf("VBAR_EL1_ADDR: %p\n", VBAR_EL1_ADDR); // GetSymbol :根据传入的地址,获取函数名。对应着NanoCode命令 ln ULONG64 FuncAddr = 0xffffff8008081800, SymOffset; char FuncName[50] = ""; GetSymbol(FuncAddr, FuncName, &SymOffset); // Disasm :根据传入的地址,获取该地址处的一条汇编指令。对应着NanoCode命令 u char Assembly[100] = ""; ULONG64 Address = 0xffffff8008081800; Disasm(&Address, Assembly, 100); }