通过GDK8观察ARM框架下的中断向量表

一、中断向量表介绍

        中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
以上是中断在百度百科中些较为浅显的解释,中断实际上是操作系统中较为重要的一种概念及机制,关于中断的一些详细的说明可以在由张银奎老师主讲的《在调试器下理解计算系统》中观看到(视频在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指向的是中断向量表。其中不同向量表间的偏移量、中断类型、中断级别可以参考下图。

referenceAnatomy 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])。

再对照下图内核源码:可以看到源码和上图表格中的分析是一一对应的。

源码Website:kernel/entry.S at main · gdk8/kernel (github.com)

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中可以使用 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);
}

猜你喜欢

转载自blog.csdn.net/iuu77/article/details/128648459