Bochs源码分析 - 8:bochs的CPU代码初步分析
前言
上一篇文章,我们分析了floppy设备的初始化,本来我们希望看看floppy的工作原理,但是调试时发现其运行cpu,通过BIOS启动指令来调用floppy。( 从现在来看这是肯定的,floppy是外设,要想调用必须通过cpu,还是自己对底层不太熟悉 )
Bochs的CPU模块特别复杂,函数众多,我们是第一次分析,对其结构还是不太熟悉,我们先尝试来看其执行流程。在分析前,我浏览了一遍喻强老师的《Bochs项目源码分析与注释》,其CPU模块的核心图如下。

CPU模块的分析思路
CPU模块太庞大了,花了很长时间来考虑如何来分析这部分代码。CPU会去读取指令并且解析指令,可CPU执行的第一条指令是哪里呢?经过资料查询,当电源通电时运行的是0xFFF0指令,然后运行BIOS一段程序来初始化各个硬件端口,之后启动bootloader,由bootloader来启动操作系统。
其BIOS代码,还记得我们的配置文件嚒?其中有关于BIOS的配置文件,如下图,这个就是bochs用的bios组件。
romimage: file= D:/code/git_local_code/kkbochs-master/bochs-src/bios/BIOS-bochs-latest
我们将该文件拖到IDA中来进行分析。( 这可以编译出来,但是不源码分析了,毕竟我们现在主要的目的是分析CPU,我们通过逆向分析其找到运行的前几个指令即可,而不是分析bios源码,之后可能会涉及这个bios,貌似是SeaBios项目? )

如上的IDA分析图,其运行的前几个指令如下。我们之后将其bochs重新编译为调试器模式,其详细操作在第四章中有所描述,然后用调试模式运行bochs来验证这几个指令是否正确。我这里验证是正确的,就不在这里赘述了。
BIOS_F:FFF0 jmp far ptr start_0
BIOS_F:E05B start_0: ; CODE XREF: sub_F5421+2BC↑J
BIOS_F:E05B ; start↓J
BIOS_F:E05B xor ax, ax
BIOS_F:E05D out 0Dh, al ; DMA controller, 8237A-5.
BIOS_F:E05D ; master clear.
BIOS_F:E05D ; Any OUT clears the ctrlr (must be re-initialized)
BIOS_F:E05F out 0DAh, al
BIOS_F:E061 mov al, 0C0h
BIOS_F:E063 out 0D6h, al
BIOS_F:E065 mov al, 0
BIOS_F:E067 out 0D4h, al
BIOS_F:E069 mov al, 0Fh
BIOS_F:E06B out 70h, al ; CMOS Memory/RTC Index Register:
BIOS_F:E06B ; shutdown status byte
BIOS_F:E06D in al, 71h ; CMOS Memory/RTC Data Register
搞到了其cpu运行的前几个指令,我们接下来调试一下cpu是如何来执行这条指令的。
cpu_loop( )函数初步解读
在第五章中,我们大体分析了bochs的启动流程,明确了其是在 cpu_loop( )函数中来启动并运行CPU的,我们先来显示其经过裁剪的代码。
void BX_CPU_C::cpu_loop(void)
{
if (setjmp(BX_CPU_THIS_PTR jmp_buf_env)) {
// can get here only from exception function or VMEXIT
BX_CPU_THIS_PTR icount++;
BX_SYNC_TIME_IF_SINGLE_PROCESSOR(0);
}
// We get here either by a normal function call, or by a longjmp
// back from an exception() call. In either case, commit the
// new EIP/ESP, and set up other environmental fields. This code
// mirrors similar code below, after the interrupt() call.
BX_CPU_THIS_PTR prev_rip = RIP; // commit new EIP
BX_CPU_THIS_PTR speculative_rsp = 0;
while (1) {
// check on events which occurred for previous instructions (traps)
// and ones which are asynchronous to the CPU (hardware interrupts)
if (BX_CPU_THIS_PTR async_event) {
if (handleAsyncEvent()) {
// If request to return to caller ASAP.
return;
}
}
bxICacheEntry_c *entry = getICacheEntry();
bxInstruction_c *i = entry->i;
static unsigned int kkCnt = 0;
for(;;) {
kkCnt += 1;
// want to allow changing of the instruction inside instrumentation callback
BX_INSTR_BEFORE_EXECUTION(BX_CPU_ID, i);
RIP += i->ilen();
// when handlers chaining is enabled this single call will execute entire trace
BX_CPU_CALL_METHOD(i->execute1, (i)); // might iterate repeat instruction
BX_SYNC_TIME_IF_SINGLE_PROCESSOR(0);
if (BX_CPU_THIS_PTR async_event) break;
i = getICacheEntry()->i;
}
// clear stop trace magic indication that probably was set by repeat or branch32/64
BX_CPU_THIS_PTR async_event &= ~BX_ASYNC_EVENT_STOP_TRACE;
} // while (1)
}
下面这段代码应该是intel虚拟化的vmexit指令,可以退回这里,这种来跨函数跳转。我们未来可能会考虑分析vmexit指令,现在先来继续往下分析。
if (setjmp(BX_CPU_THIS_PTR jmp_buf_env)) {
// can get here only from exception function or VMEXIT
//
BX_CPU_THIS_PTR icount++;
BX_SYNC_TIME_IF_SINGLE_PROCESSOR(0);
}
下面这代码应该是来处理异步事件的,什么是异步事件,外设中断硬件中断应该就是异步事件,当这个事件发生时,其不能立刻被执行,因为此时CPU可能在运转,当CPU运行完空闲时,其会来处理这个事件。
// check on events which occurred for previous instructions (traps)
// and ones which are asynchronous to the CPU (hardware interrupts)
if (BX_CPU_THIS_PTR async_event) {
if (handleAsyncEvent()) {
// If request to return to caller ASAP.
return;
}
}
下面这段代码,先获取缓存,从缓存中来读取下一条要执行的指令。
bxICacheEntry_c *entry = getICacheEntry();
bxInstruction_c *i = entry->i;
下面这段代码很好理解,其就是循环执行指令的。注意看代码中的注释,我们可以通过调用"BX_INSTR_BEFORE_EXECUTION(..)"该函数来修改某条代码执行的指令。其调用"BX_CPU_CALL_METHOD(..)"该函数来执行指令,再执行之前来计算该条指令的RIP值。 执行完之后发现如果存在异步事件,则break退出去执行,否则就再从缓冲区来读取下一条指令,这些很好理解。
for(;;) {
// want to allow changing of the instruction inside instrumentation callback
BX_INSTR_BEFORE_EXECUTION(BX_CPU_ID, i);
RIP += i->ilen();
// when handlers chaining is enabled this single call will execute entire trace
BX_CPU_CALL_METHOD(i->execute1, (i)); // might iterate repeat instruction
BX_SYNC_TIME_IF_SINGLE_PROCESSOR(0);
if (BX_CPU_THIS_PTR async_event) break;
i = getICacheEntry()->i;
}
CPU的调试分析思路
我们想用visual studio来调试bochs虚拟机,但是里面运行的代码来调试就很麻烦了,在困扰我很久之后想出了一种调试思路:在visual studio中设置条件断点来控制住RIP,然后用IDA来分析bios与a.img,确定其RIP地址。
初步调试jmp指令
经过我们上面分析,bochs通电之后RIP初始化为0xFFF0,其第一条指令为 "jmp0xf000:E05B",其中 i->execute1 是该条指令执行的下一个函数,通过BX_CPU_CALL_METHODB(..)宏来实现。
# define BX_CPU_CALL_METHOD(func, args) \
((BxExecutePtr_tR) (func)) args
下面是其调用的堆栈图,我进行了一些基本的简化,其该指令执行比较简单,首先是加载段寄存器,然后来获取EIP偏移进行赋值,这段代码还是很好理解的。
void BX_CPP_AttrRegparmN(1) BX_CPU_C::JMP_Ap(bxInstruction_c *i)
| // 获取display
|---> disp32 = i->Iw();
| // 获取段寄存器
|---> cs_raw = i->Iw2();
||
|-->jmp_far32(bxInstruction_c *i, Bit16u cs_raw, Bit32u disp32)
| // 加载段寄存器, cs_raw = 0x9
|---> load_seg_reg(&BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS], cs_raw);
| // 修改EIP
|---> EIP = disp32;
|--> return;
修改BX_NEXT_INSTR宏禁止指令递归执行
在调试代码中发现EIP值一直对不上,经过分析发现某些指令函数中存在BX_NEXT_INSTR(...)宏,该宏可以直接执行下一个条指令,而不是返回到cpu_loop( )函数的循环中获取下一个指令,这种宏的存在会影响我们的分析。
void BX_CPP_AttrRegparmN(1) BX_CPU_C::ZERO_IDIOM_GwR(bxInstruction_c *i)
{
BX_WRITE_16BIT_REG(i->dst(), 0);
SET_FLAGS_OSZAPC_LOGIC_16(0);
BX_NEXT_INSTR(i); // < ------ 直接在指令执行函数中执行下一条指令
}
#define BX_NEXT_INSTR(i) { \
BX_COMMIT_INSTRUCTION(i); \
if (BX_CPU_THIS_PTR async_event) return; \
++i; \
BX_EXECUTE_INSTRUCTION(i); \
}
我们下面直接来取消这个宏,其并不会影响程序正常执行,如下代码修改:
#define BX_NEXT_INSTR(i)
#define _BX_NEXT_INSTR(i) { \
BX_COMMIT_INSTRUCTION(i); \
if (BX_CPU_THIS_PTR async_event) return; \
++i; \
BX_EXECUTE_INSTRUCTION(i); \
}
总结
我们现在初步了解了cpu模块的代码,确定了分析思路,并且修改了代码,方便了之后我们的代码调试。接下来我们计划分析 in/out 指令的执行流程,毕竟我们通过 in/out 指令来与其他设备进行交互,然后来分析异步事件的机制async_event。
我们现在先不详细分析CPU机制,因为我们目前还是在实模式中,CPU的重点是保护模式。关于保护模式时间机制以及如果模拟intel虚拟化这些问题,我们打算之后结合邓志的《X86/X64体系探索及编程 》,《处理器虚拟化技术》以及《Intel开发者手册》这三本书来详细分析。

