Advertisement

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开发者手册》这三本书来详细分析。

全部评论 (0)

还没有任何评论哟~