LLVM 3.1 測試經驗談

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

*** 此篇文章技術性不高,純粹是分享替 LLVM 做測試的經驗。***

一切都從 [3.1 Release] Call For Testers! 這一封徵求 LLVM 3.1 Release Tester 的信開始。我純粹抱著試試看的心態,回信給 Bill Wendling 問他新手如我是否也能參與 LLVM 3.1 Release Testing。在約莫一個禮拜之後,Bill 通知所有曾經回信給他的人關於 LLVM Testing 大致的流程以及相關注意事項。我這裡簡單做個摘要:

    1. 一開始 Bill 會要求所有測試者寄給他測試平台的相關資訊,諸如作業系統和編譯器版本。Bill 需要這個資訊以便知道測試覆蓋程度如何。以我的例子,我會回覆他:

    Two pandaboard:

    arm linux (ubuntu 11.04), gcc 4.5.2

    注意! 基本上不鼓勵在測試期間更新作業系統和編譯器版本。

    2. Bill 會在郵件列表上公布測試的時程表,一般測試為期一個月。這次公布的時程如下:

    April 16th: Branching for 3.1 release
    April 16-22: Phase 1 testing
    April 23-29: Bug fixing for Phase 1 issues, all features completed
    April 30-May 6: Phase 2 testing
    May 7-13: Addressing Phase 2 issues, final binary generation
    May 14th: Release of 3.1!

    基本上是維持一個禮拜測試,下一個禮拜修正錯誤的循環。Bill 會視情況決定是否延長測試的時間。

    3. 在測試期間所發現的臭蟲,一律送交 Bugzilla。Bill 會要求你回報臭蟲的同時,將他加進 CC list 以便他監控臭蟲修正的進度。基本上,只要不是太嚴重的臭蟲都不會阻礙 LLVM release 的時程。另外,非主流平台 (x86 除外) 上的臭蟲通常都需要測試者幫忙修正。測試者可以在郵件列表或是聊天室尋求幫助。
    4. ${LLVM_SRC}/utils/release 目錄底下提供幾個腳本協助測試者進行測試。測試分為兩類: 一類是所謂的回歸測試 (regression test),這類測試位在 ${LLVM_SRC}/test 底下。主要用來確保 LLVM/Clang 的正確性。第二類是評量 LLVM 在效能上是否有退步,這類測試位在 ${LLVM_SRC}/projects/test-suite。測試分底下三個步驟:

    a. 以不同模式 (Debug、Assert 或是 Release) 編譯 LLVM/Clang 3.1 pre-release
    candidate,也就是所謂的 RC (release candidate),並進行回歸測試。這一步最為
    重要,事實上我這次的測試也只有做到這一步。

    b. 下載 LLVM/Clang 3.0 代碼並編譯,運行 test-suite 得到一組分數作為比較基準
    (baseline)。

    c. 以編譯好的 LLVM/Clang 3.1 pre-release candidate “手動” 運行 test-suite,
    並得到一組分數。將此分數與前述 baseline 做比較,若有退步便回報。

    整個測試工具的使用可以參考 LLVM Testing Infrastructure Guide

O.K.,現在就等 LLVM 從 trunk 替 3.1 release 開一個 branch,接著就可以準備測試。Bill 會在郵件列表上發布 3.1 Has Branched 的消息。但別急! 一切都等待 Bill 這位 release manager 的指示,他會通知測試者 release candidate 的代碼該從哪裡下載,並給出較之前為詳細的指示。你可能會收到類似 “Release Candidate 1 Ready for Testing” 的信件,我這裡簡單做個摘要:

    1. 隨信的附件中會有幾個腳本供測試者使用,你同樣可以在 ${LLVM_SRC}/utils/release 找到底下幾個腳本:

    • test-release.sh
    • findRegressions-simple.py
    • findRegressions-nightly.py

    後兩個是用來比較 test-suite 分數。我主要使用的是 test-release.sh 這支腳本。

    2. Bill 會描述如何使用 test-release.sh。以測試 LLVM 3.1 RC1 為例,

    $ test-release.sh -release 3.1 -rc 1 -no-64bit -no-compare-files

    因為我是測試 ARM 平台,所以不測試 64 bit。整個編譯流程跟 GCC 極為類似,總共
    有三個階段。Phase 1 是以系統工具鏈編譯 LLVM/Clang,Phase 2 是使用 Phase 1
    編譯所得的 clang 編譯 LLVM/Clang,Phase 3 是使用 Phase 2 編譯所得的 clang
    編譯 LLVM/Clang,最後比較 Phase 2 和 Phase 3 的結果。因為某些不知名的原因,
    Bill 稱此次 Phase 2 與 Phase 3 的目的檔有所不同,但可忽略,他建議我們使用
    -no-compare-files 跳過 Phase 2 與 Phase 3 的比較。`test-release.sh -help`
    可以得到更多訊息。注意! 請確定最後執行回歸測試所用的是 Phase 3 的
    LLVM/Clang。剛開始進行測試的時候,我多下了 “-disable-clang” 這個選項,導致
    只有 Phase 1 的結果,用 Phase 1 進行回歸測試得到的結果是不準確的。回歸測試
    的結果相當重要,有臭蟲皆需要立即回報給 Bugzilla (http://llvm.org/bugs),
    並 CC 給 Bill,由他決定該臭蟲是否會阻礙 LLVM release 的時程。

    3. 分別以 LLVM 3.0 和 LLVM 3.1 RC 運行 test suite,並以 findRegressions-simple.py
    和 findRegressions-nightly.py 比較兩者的結果。這次我並沒有認真地運行 test
    suite。
    4. 打包 Phase 3 所得結果,並交由 Bill 上傳至 LLVM 官網。基本指令如下:

    $ cd rc1/Phase3/Release
    $ tar zcvf llvmCore-3.1-rc1.install-arm-ubuntu_11.04.tar.gz llvmCore-3.1-rc1.install/

至此就算是告一個段落,爾後的 RC2 亦或是 RC3 基本上都是依循上述流程。這次測試過程中有個小插曲,RC2 的代碼在 ARM 上編譯不過。這時要盡快回報給 Bill,同時自己這邊也要做 bisect 抓出出錯的版本號。基本流程如下:

    1. 取得正常可運作的版本號,這裡我取 RC1 (r155062)。

    $ svn log http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_31/rc1 –stop-on-copy
    ————————————————————————
    r155062 | void | 2012-04-19 06:10:56 +0800 (四, 19 4 2012) | 1 line

    Creating release candidate rc1 from release_31 branch

    2. 取得出錯的版本號,這裡我取 RC2 (r156037)。

    $ svn log http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_31/rc2 –stop-on-copy
    ————————————————————————
    r156037 | void | 2012-05-03 08:11:00 +0800 (四, 03 5 2012) | 1 line

    Creating release candidate rc2 from release_31 branch

    3. 透過 svn-bisect 用二分法找出一開始出問題的版本。注意! LLVM 和 Clang
    分屬不同的 SVN,但是在糾錯時請保持 LLVM 和 Clang 的版本一致。

    $ cd rc2/llvm.src
    # 好 壞
    $ svn-bisect start 155062 156037
    Switching to r155672 …
    # 手動將 Clang 切換至 r155672
    $ cd rc2/cfe.src
    $ svn up -r 155672
    # 開始編譯。-no-checkout 是要求直接使用當前目錄下的代碼,不從 SVN checkout。
    $ cd rc2
    $ ./test-release.sh -release 3.1 -rc 2 -no-checkout -no-64bit -no-compare-files
    # 如果 r155672 編譯出錯,將其標定為 bad,svn-bisect 會切換到下一個應該要檢測的
    # 版本。
    $ svn-bisect bad 155672
    Switching to r155374 …

    幸運的是,在我 bisect 出錯誤版本之前,Bill 已經先我一步找到出問題的版本號。
    要知道在 ARM 機器上編譯 LLVM/Clang 可以頗為耗時的一件事。:-)

    4. 等待 Bill 重新發布下一個修正過後的測試版本,RC3。

最後進入 final release 時,測試者只需編譯好 LLVM/Clang,無須再跑回歸測試或是 test suite,直接將 Phase 3 打包送給 Bill 即可。剩下的就是靜待 LLVM 3.1 Release
的發布。我這裡提一下提交臭蟲至 Bugzilla 時,應該要有的格式。以 Test case Sema/arm-neon-types.c fail on ARM 為例,標題寫明是在 ARM 平台上,Sema/arm-neon-types.c 這個測試失敗。在內容描述裡面,把 `make check-all` 所吐出的訊息貼上去。基本上 test-release.sh 這支腳本在編譯完 Phase 3 LLVM/Clang 後會運行回歸測試,即 `make check-all`,同時會留下 log。你只需要將該 log 的內容檢視一遍,將出錯的測試回報給 Bugzilla 即可。非 x86 平台的測試者需要自己主動修正錯誤,你可以在郵件列表和聊天室尋求幫助。

修正臭蟲之後就可以送 patch,以 Fix test case failure due to C++ ABI difference on ARM 為例,標題簡明扼要的描述這個 patch 修正了什麼問題,信件正文再較為詳細的敘述這個 patch 的意圖,必要時附帶上輔助資料,最後以附件形式附上 patch。

整個測試過程中,如果有 “任何” 疑問,不要遲疑,發信到郵件列表或是 release manager 尋求協助。最後,你可以在 Chris Lattner 發布的 LLVM 3.1 Release! 公告上看見你的名字。

本篇文章沒有技術含量,純粹是希望鼓勵更多人參與 LLVM 的開發,不論是以何種形式。:-)

[1] http://lists.cs.uiuc.edu/pipermail/llvmdev/2012-March/048355.html
[2] http://llvm.org/docs/TestingGuide.html
[3] http://lists.cs.uiuc.edu/pipermail/llvmdev/2012-April/048961.html
[4] http://llvm.org/bugs/show_bug.cgi?id=12694
[5] http://lists.cs.uiuc.edu/pipermail/cfe-commits/Week-of-Mon-20120430/056982.html
[6] http://lists.cs.uiuc.edu/pipermail/llvm-announce/2012-May/000041.html

QEMU Internal – Precise Exception Handling 5/5

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

最後,如同我之前所承諾的。我們來看 tlb_fill 從一般 C 函式和 code cache 被呼叫是什麼意思。

我們可以看到 retaddr == 0 時,tlb_fill 是從一般 C 函式被呼叫。

(gdb) b tlb_fill
(gdb) r -boot a -fda Image -hda hdc-0.11-new.img -vnc 0.0.0.0:1 -d in_asm,op,out_asm
Breakpoint 1, tlb_fill (addr=4294967280, is_write=2, mmu_idx=0, retaddr=0x0) at
/tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
4816    {
(gdb) bt
#0  tlb_fill (addr=4294967280, is_write=2, mmu_idx=0, retaddr=0x0) at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
#1  0x000000000050ee86 in __ldb_cmmu (addr=4294967280, mmu_idx=0) at /tmp/chenwj/qemu-0.13.0/softmmu_template.h:134
#2  0x000000000051045e in ldub_code (ptr=4294967280) at /tmp/chenwj/qemu-0.13.0/softmmu_header.h:87
#3  0x000000000051054b in get_page_addr_code (env1=0x110e390, addr=4294967280) at /tmp/chenwj/qemu-0.13.0/exec-all.h:325
#4  0x0000000000510986 in tb_find_slow (pc=4294967280, cs_base=4294901760, flags=68) at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:139
#5  0x0000000000510b9d in tb_find_fast () at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:188
#6  0x00000000005112db in cpu_x86_exec (env1=0x110e390) at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:575
#7  0x000000000040aabd in qemu_cpu_exec (env=0x110e390) at /tmp/chenwj/qemu-0.13.0/cpus.c:767
#8  0x000000000040abc4 in cpu_exec_all () at /tmp/chenwj/qemu-0.13.0/cpus.c:795
#9  0x000000000056e417 in main_loop () at /tmp/chenwj/qemu-0.13.0/vl.c:1329
#10 0x00000000005721cc in main (argc=11, argv=0x7fffffffe1a8, envp=0x7fffffffe208) at /tmp/chenwj/qemu-0.13.0/vl.c:2992

我們可以看到 retaddr != 0 時,tlb_fill 是從 code cache 中被呼叫。

Breakpoint 1, tlb_fill (addr=28668, is_write=1, mmu_idx=0, retaddr=0x4000020c)
at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
4816    {
(gdb) bt
#0  tlb_fill (addr=28668, is_write=1, mmu_idx=0, retaddr=0x4000020c) at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
#1  0x000000000054e511 in __stl_mmu (addr=28668, val=982583, mmu_idx=0) at /tmp/chenwj/qemu-0.13.0/softmmu_template.h:272
#2  0x000000004000020d in ?? ()  <--- 我們在 code cache 裡!

我們來看一下 qemu.log 驗證一下我們對 QEMU 的了解。;) 既然 retaddr = 發生例外的 host binary 下一條指令位址減去 1,我們定位到 0x4000020d。

0x40000208:  callq  0x54e3a0
0x4000020d:  movzwl %bx,%ebp

瞧瞧 __stl_mmu 的位址,果然是 0x54e3a0。這代表我們在 code cache 呼叫 __stl_mmu。__stl_mmu 再去呼叫 tlb_fill 的時候發生例外。

(gdb) p __stl_mmu
$1 = {void (target_ulong, uint32_t, int)} 0x54e3a0

這裡我們可以看到 SOFTMMU 相關的 helper function 在各個地方都會被用到,不論是 QEMU 本身的函式 (一般 C 函式) 或是 code cache 都會調用 __{ld,st}{b,w,l,q}_{cmmu,mmu}。這些 helper function 又會調用 tlb_fill。tlb_fill 就是透過 retaddr 來判定是否需要回復 guest CPUState。

QEMU Internal – Precise Exception Handling 4/5

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

好! 我們現在找到例外 (本範例是頁缺失) 是發生在某個 TranslationBlock 裡頭,但是到底是哪一條 guest 指令觸發頁缺失? 我們需要從頭翻譯該 TranslationBlock 對應的 guest binary 來揪出罪魁禍首。一般情況下,QEMU 在翻譯 guest binary 時不會記錄 guest pc 資訊。這時,為了定位 guest pc,QEMU 在翻譯 guest binary 會記錄額外的資訊,包含 guest pc。

QEMU 會用到底下定義在 translate-all.c 資料結構:

  target_ulong gen_opc_pc[OPC_BUF_SIZE]; // 紀錄 guest pc。
  uint8_t gen_opc_instr_start[OPC_BUF_SIZE]; // 當作標記之用。

針對 x86,又在 target-i386/translate.c 定義以下資料結構:

  static uint8_t gen_opc_cc_op[OPC_BUF_SIZE]; // 紀錄 condition code。

現在來看 cpu_restore_state (translate-all.c)。searched_pc 傳入的 (幾乎) 是發生例外的 host pc。

int cpu_restore_state(TranslationBlock *tb,
                      CPUState *env, unsigned long searched_pc,
                      void *puc)
{
    tcg_func_start(s); // 初始 gen_opc_ptr 和 gen_opparam_ptr
 
    // 轉呼叫 gen_intermediate_code_internal,要求在生成 TCG IR
    // 的同時,為其生成相關的 guest pc 和其它資訊於下列資料結構。
    //
    //   gen_opc_pc, gen_opc_instr_start, 和 gen_opc_cc_op
    //
    gen_intermediate_code_pc(env, tb);
 
    // 轉呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 翻成 host binary。
    // 返回 TCG gen_opc_buf index。
    j = tcg_gen_code_search_pc(s, (uint8_t *)tc_ptr, searched_pc - tc_ptr);
 
    // gen_opc_instr_start[j] 若為 1,代表 gen_opc_pc[j] 和 gen_opc_cc_op[j]
    // 正是我們所要的資訊。
    while (gen_opc_instr_start[j] == 0)
        j--;
 
    // 回復 CPUState。
    gen_pc_load(env, tb, searched_pc, j, puc);
 
}

gen_intermediate_code_pc 是 gen_intermediate_code_internal 的包裝,search_pc 設為 1。當 search_pc 為 true,在翻譯 guest binary 的同時,生成額外資訊。

static inline void gen_intermediate_code_internal(CPUState *env,
                                                  TranslationBlock *tb,
                                                  int search_pc)
{
    // guest binary -&gt; TCG IR
    for(;;) {
 
        if (search_pc) {
            // gen_opc_ptr 為 TCG opcode buffer 目前位址,gen_opc_buf 為
            // TCG opcode buffer 的起始位址。
            j = gen_opc_ptr - gen_opc_buf;
            if (lj &lt; j) {
                lj++;
                while (lj cc_op; // 紀錄 condition code。
            gen_opc_instr_start[lj] = 1; // 填 1 作為標記。
            gen_opc_icount[lj] = num_insns;
        }
 
        // 針對 pc_ptr 代表的 guest pc 進行解碼並生成 TCG IR,返回下一個 guest pc。
        pc_ptr = disas_insn(dc, pc_ptr);
 
    }
}

tcg_gen_code_search_pc 是 tcg_gen_code_common 的包裝,search_pc (應命名為 offset) 設為發生例外的 host binary 與其所屬 basic block 在 code cache 開頭 (tc_ptr) 的 offset。注意! 此時傳入 gen_code_buf 的是觸發例外的 TranslationBlock 其 tc_ptr。也就是說,現在 TCG IR -> host binary 中的 host binary 是寫在發生例外 host binary 所屬 basic block 在 code cache 的開頭。我們把這段 host binary 覆寫了! 當然寫的內容和被覆寫的內容一模一樣。我們只想要透過這個方式反推觸發例外的 guest pc。

static inline int tcg_gen_code_common(TCGContext *s, uint8_t *gen_code_buf,
                                      long search_pc)
{
    for(;;) {
        switch(opc) {
        case INDEX_op_nopn:
            args += args[0];
            goto next;
        case INDEX_op_call:
            dead_args = s-&gt;op_dead_args[op_index];
            args += tcg_reg_alloc_call(s, def, opc, args, dead_args);
            goto next;
        }
        args += def-&gt;nb_args;
    next:
        // 如果 offset (search_pc) 落在 tc_ptri (gen_code_buf) 和 code cache
        // 目前存放 host binary 的位址之間, 返回 TCG gen_opc_buf index。
        if (search_pc &gt;= 0 &amp;&amp; search_pc <s>code_ptr - gen_code_buf) {
            return op_index;
        }
        op_index++;
    }
}

此時,gen_opc_pc 和 gen_opc_cc_op 已存放發生例外的 guest pc 和當時的 condition code。gen_pc_load 負責回復 CPUState。

void gen_pc_load(CPUState *env, TranslationBlock *tb,
                unsigned long searched_pc, int pc_pos, void *puc)
{
    env-&gt;eip = gen_opc_pc[pc_pos] - tb-&gt;cs_base;
    cc_op = gen_opc_cc_op[pc_pos];
}

至此,CPUState 已完全回復,我們回來看 tlb_fill。raise_exception_err (target-i386/op_helper.c) 這時候拉起虛擬 CPU 的 exception_index (env->exception_index),並設置 error_code (env->error_code)。

void tlb_fill(target_ulong addr, int is_write, int mmu_idx, void *retaddr)
{
    ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx, 1);
    if (ret) {
        if (retaddr) {
 
            // 當客戶發生頁缺失 (ret == 1) 且 tlb_fill 是從 code cache 中被
            // 呼叫 (retaddr != 0),我們會在這裡。
 
            /* now we have a real cpu fault */
            pc = (unsigned long)retaddr;
            tb = tb_find_pc(pc);
            if (tb) {
                /* the PC is inside the translated code. It means that we have
                   a virtual CPU fault */
                cpu_restore_state(tb, env, pc, NULL);
            }
        }
        raise_exception_err(env-&gt;exception_index, env-&gt;error_code);
    }
    env = saved_env;
}

raise_exception_err 實際上是 raise_interrupt 的包裝 (wrapper)。QEMU_NORETURN 前綴代表此函式不會返回。它其實是 GCC 擴展 __attribute__ ((__noreturn__)),定義在 qemu-common.h [1]。

static void QEMU_NORETURN raise_interrupt(int intno, int is_int, int error_code,
                                          int next_eip_addend)
{
    ... 略 ...
 
    env-&gt;exception_index = intno;
    env-&gt;error_code = error_code;
    env-&gt;exception_is_int = is_int;
    env-&gt;exception_next_eip = env-&gt;eip + next_eip_addend;
    cpu_loop_exit();
}

cpu_loop_exit (cpu-exec.c) 用 longjmp 返回至 cpu_exec (cpu-exec.c) 中處理例外的分支。

void cpu_loop_exit(void)
{
    env-&gt;current_tb = NULL;
    longjmp(env-&gt;jmp_env, 1);
}

來看 cpu_exec。cpu_exec 裡用到許多 #ifdef,強烈建議查看經過預處理之後結果,即 ${BUILD}/i386-softmmu/cpu-exec.i 中的 cpu_x86_exec。

int cpu_exec(CPUState *env)
{
    // 進行翻譯並執行的迴圈。
    /* prepare setjmp context for exception handling */
    for(;;) {
        if (setjmp(env-&gt;jmp_env) == 0) { // 正常流程。
            /* if an exception is pending, we execute it here */
            if (env-&gt;exception_index &gt;= 0) {
 
              /* 2. 再來到這裡,處理例外。 */
 
            }
 
            next_tb = 0; /* force lookup of first TB */
            for(;;) {
 
            } /* inner for(;;) */
        }
 
        /* 1. 我們先來到這裡。 */
 
    } /* outer for(;;) */
}

O.K.,到這裡就是一個循環。:-) 接著,我們來驗證一下我們對 QEMU 的理解。

[1] http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html

QEMU Internal – Precise Exception Handling 3/5

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

首先我們來看 __stb_mmu。請看 ${SRC}/softmmu_template.h 和 ${BUILD}/i386-softmmu/op_helper.i。SOFTMMU 相關的 helper function 是透過 softmmu_* 檔案內的巨集加以合成。這裡只挑部分加以描述。

SUFFIX 可以是 b (byte, 8)、w (word, 16)、l (long word, 32) 和 q (quad word, 64),代表資料大小。MMUSUFFIX 可以是 cmmu 或是 mmu,分別代表欲讀取的是 code 或是 data。mmu_idx 代表索引的是內核態亦或是用戶態的 TLB。addr 代表 guest virtual address。

// ${SRC}/softmmu_template.h
void REGPARM glue(glue(__st, SUFFIX), MMUSUFFIX)(target_ulong addr,
                                                 DATA_TYPE val,
                                                 int mmu_idx)
{
   ...
}

接著看展開巨集後的函式體。

// ${BUILD}/i386-softmmu/op_helper.i
void __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx)
{
 redo:
    // 查找 TLB
    tlb_addr = env-&gt;tlb_table[mmu_idx][index].addr_write;
    if (...) {
 
        // TLB 命中
 
    } else {
 
        // TLB 不命中
 
        /* the page is not in the TLB : fill it */
        // retaddr = GETPC();
        retaddr = ((void *)((unsigned long)__builtin_return_address(0) - 1));
 
        // 試圖填入 TLB entry。
        tlb_fill(addr, 1, mmu_idx, retaddr);
        goto redo;
    }
}

這裡 QEMU 利用 GCC 的 __builtin_return_address 擴展 [1] 來判定 tlb_fill 是從一般 C 函式或是 code cache 中被呼叫。retaddr 若為 0,表前者,retaddr 若不為 0,表後者。之後,我們會透過 GDB 更加清楚前面所述所代表的意思。我們關注 retaddr 不為 0,也就是從 code cache 中呼叫 tlb_fill 的情況。

在看 tlb_fill 之前,我們先偷看 cpu_x86_handle_mmu_fault (target-i386/helper.c) 的註解。我們關注返回值為 1,也就是頁缺失的情況。

/* return value:
   -1 = cannot handle fault
   0  = nothing more to do
   1  = generate PF fault
*/
int cpu_x86_handle_mmu_fault(CPUX86State *env, target_ulong addr, ...)
{
  ...
}

我們來看 tlb_fill。

void tlb_fill(target_ulong addr, int is_write, int mmu_idx, void *retaddr)
{
    ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx, 1);
    if (ret) {
        if (retaddr) {
 
            // 當客戶發生頁缺失 (ret == 1) 且 tlb_fill 是從 code cache 中被
            // 呼叫 (retaddr != 0),我們會在這裡。
 
            /* now we have a real cpu fault */
            pc = (unsigned long)retaddr;
            tb = tb_find_pc(pc);
            if (tb) {
                /* the PC is inside the translated code. It means that we have
                   a virtual CPU fault */
                cpu_restore_state(tb, env, pc, NULL);
            }
        }
        raise_exception_err(env-&gt;exception_index, env-&gt;error_code);
    }
    env = saved_env;
}

請注意! 如果 retaddr != 0,其值代表的 (幾乎) 是發生例外的 host binary 所在位址。QEMU 利用它來查找是哪一個 TranslationBlock 中的 host binary 發生例外。tb_find_pc (exec.c) 利用該 host binary pc 進行查找,取得 tb。

TranslationBlock *tb_find_pc(unsigned long tc_ptr)
{
    // tbs 是 TranslationBlock * 數組。每一個在 code cache 中 (已翻譯好的)
    // basic block 都有相對應的 TranslationBlock 存放其相關資訊。
 
    /* binary search (cf Knuth) */
    m_min = 0;
    m_max = nb_tbs - 1;
    while (m_min &gt; 1;
        tb = &amp;tbs[m];
        // tc_ptr 代表 host binary 在 code cache 的起始位址。
        v = (unsigned long)tb-&gt;tc_ptr;
        if (v == tc_ptr)
            return tb;
        else if (tc_ptr &lt; v) {
            m_max = m - 1;
        } else {
            m_min = m + 1;
        }
    }
    return &amp;tbs[m_max];
}

一但找到該負責的 tb,QEMU 就會回復 guest CPUState 以便 guest exception handler 處理 guest 的頁缺失例外。

    if (tb) {
        /* the PC is inside the translated code. It means that we have
           a virtual CPU fault */
        cpu_restore_state(tb, env, pc, NULL);
    }

接著我們看 cpu_restore_state (translate-all.c)。

[1] http://gcc.gnu.org/onlinedocs/gcc/Return-Address.html

QEMU Internal – Precise Exception Handling 2/5

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

這裡以 linux-0.11 當範例,請至 [1][2] 下載代碼和硬盤映像編譯。我們觀察以 guest pc 0xe4c0 為開頭的 basic block。使用 QEMU 0.13 [3] 運行 linux-0.11。

$ mkdir build; cd build
$ ../qemu-0.13.0/configure --prefix=$INSTALL --target-list=i386-softmmu \
    --enable-debug --extra-cflags="--save-temps"
$ make install
$ gdb qemu
(gdb) r -boot a -fda Image -hda hdc-0.11-new.img -vnc 0.0.0.0:1 -d in_asm,op,out_asm

登入後,下 `ls`。觀察 qemu.log 並定位至 0xe4c0。首先,我們可以看到如下內容:

IN:
0x0000e4c0:  sub    $0x4,%esp
0x0000e4c3:  mov    0x8(%esp),%eax
0x0000e4c7:  mov    %al,(%esp)
0x0000e4ca:  movzbl (%esp),%eax
0x0000e4ce:  mov    0xc(%esp),%edx
0x0000e4d2:  mov    %al,%fs:(%edx)
0x0000e4d5:  add    $0x4,%esp
0x0000e4d8:  ret

OP:
 ---- 0xe4c0
 movi_i32 tmp1,$0x4
 mov_i32 tmp0,esp
 sub_i32 tmp0,tmp0,tmp1
 mov_i32 esp,tmp0
 mov_i32 cc_src,tmp1
 mov_i32 cc_dst,tmp0

 ... 略 ...

OUT: [size=450]
0x40bbeff0:  mov    0x10(%r14),%ebp
0x40bbeff4:  sub    $0x4,%ebp

 ... 略 ...

0x4011813b:  callq  0x54d38a
0x40118140:  mov    0x10(%r14),%ebp

 ... 略 ...

這是 QEMU 第一次遇到尚未翻譯,以 guest pc 0xe4c0 開頭的 basic block 時所產生的輸出,這包括 guest binary (IN: 以下內容)、TCG IR (OP: 以下內容) 和 host binary (OUT: 以下內容)。

再繼續往下搜尋 0xe4c0,會看到以下內容:

IN:
0x0000e4c0:  sub    $0x4,%esp
0x0000e4c3:  mov    0x8(%esp),%eax
0x0000e4c7:  mov    %al,(%esp)
0x0000e4ca:  movzbl (%esp),%eax
0x0000e4ce:  mov    0xc(%esp),%edx
0x0000e4d2:  mov    %al,%fs:(%edx)
0x0000e4d5:  add    $0x4,%esp
0x0000e4d8:  ret

OP:
 ---- 0xe4c0
 movi_i32 tmp1,$0x4
 mov_i32 tmp0,esp
 sub_i32 tmp0,tmp0,tmp1
 mov_i32 esp,tmp0
 mov_i32 cc_src,tmp1
 mov_i32 cc_dst,tmp0

 ... 略 ...

RESTORE:
0x0000: 0000e4c0
0x0007: 0000e4c3
0x000d: 0000e4c7
0x0011: 0000e4ca
0x0015: 0000e4ce
0x001b: 0000e4d2
spc=0x4011813f pc_pos=0x1b eip=0000e4d2 cs_base=0

這裡就是重點了。spc 指的是發生例外的 host pc,eip 指的是與其相對映發生例外的 guest pc。這邊請注意,由於我們將 guest binary 翻譯成 host binary 並執行,真正發生例外的是 host binary (位於 code cache),但是我們必須將它映射回 guest pc,查出哪一條 guest 指令發生例外,並做後續處理。我們看一下第一次翻譯 0xe4d2 所得的 host binary。

0x4011813b:  callq  0x54d38a
0x40118140:  mov    0x10(%r14),%ebp

我們可以看到 spc 0x4011813f == 0×40118140 – 1,也就是 callq 0x54d38a 下一條指令所在位址減去 1。這裡做點弊,我們在 gdb 下 print __stb_mmu。

(gdb) print __stb_mmu
$1 = {void (target_ulong, uint8_t, int)} 0x54d38a

可以得知,我們在呼叫 __stb_mmu 的時候發生例外。__{ld,st}{b,w,l,q}_{cmmu,mmu} 是用來存取 guest 內存的 helper function。它們首先會查找 TLB (env1->tlb_table) 試圖取得 guest virtual address 相對映的 host virtual address。如果 TLB 命中,可直接利用該 host virtual address 存取 guest 內存內容。如果 TLB 不命中,則會呼叫 tlb_fill (target-i386/op_helper.c)。tlb_fill 會呼叫 cpu_x86_handle_mmu_fault 查找客戶頁表。如果命中,代表該 guest virtual address 所在的頁已存在,tlb_fill 會將該頁項目填入 TLB 以便後續查找。如果不命中,代表發生頁缺失,QEMU 會回復 guest CPUState,並拉起 guest exception index (env->exception_index) 通知 guest 頁缺失發生。最後交由 guest 頁缺失 handler 將該頁載入。

我先給出一個 precise exception handling 的大致流程,之後再透過閱讀代碼有更深的體會。底下給出關鍵的資料結構 TranslationBlock,它負責掌管 guest binary 和 host binary 的關係,其中 pc 代表此 basic block 起始的 guest pc,tc_ptr 指向翻譯好的 host binary 在 code cache 中的位址。

                                                                     code cache
          guest binary                   TranslationBlock           (host binary)

0x0000e4c0:  sub    $0x4,%esp         0x40bbeff0:  mov 0x10(%r14),%ebp
0x0000e4ca:  movzbl (%esp),%eax                                 0x40bbeff4:  sub $0x4,%ebp
0x0000e4ce:  mov    0xc(%esp),%edx
0x0000e4d2:  mov    %al,%fs:(%edx)                                      ... 略 ...
0x0000e4d5:  add    $0x4,%esp
0x0000e4d8:  ret                                                0x4011813b: callq  0x54d38a
                                               (3) offset  -->  0x40118140: mov    0x10(%r14),%ebp (1)

                                                                        ... 略 ...

當 QEMU 發現例外是發生在 code cache 裡,這代表需要處理 precise exception。首先,QEMU 會透過 host pc (0x4011813f) 查出相對應的 TranslationBlock – (1)。接下來,QEMU 會回復 guest CPUState。主要概念是這樣,透過之前查找到的 TranslationBlock 的 pc,我們從該 pc 所指的 guest binary 重新再翻譯一次,同時產生額外的資訊以便回復 guest CPUState – (2)。那要如何得知我們已經翻譯到發生 exception 的 guest binary? 這裡的重點在於,我们重新翻译 guest binary,直到 host binary 地址到达了出现异常的位置,这个时候 guest pc 就是产生异常的指令。我們便可以回復 guest CPUState – (3)。

接下來各小節的主題分別是:

* Precise Exception Handling 3/5 – (1)

* Precise Exception Handling 4/5 – (2) (3)

* Precise Exception Handling 5/5 – 驗證我們對 QEMU 的理解。

接下來,我們來看代碼。;)

[1] http://www.oldlinux.org/oldlinux/viewthread.php?tid=13681&extra=page%3D1
[2] http://oldlinux.org/Linux.old/bochs/linux-0.11-devel-060625.zip
[3] http://wiki.qemu.org/download/qemu-0.13.0.tar.gz