#一、使用gdb分析QEMU代碼#
使用gdb不僅可以很好地調試代碼,也可以利用它來動態地分析代碼。使用gdb調試QEMU需要做一些準備工作:
1, 編譯QEMU時需要在執行configure腳本時的參數中加入--enable-debug。
2, 從QEMU官方網站上下載一個精簡的鏡像——linux-0.2.img。linux-0.2.img只有8MB大小,啓動後包含一些常用的shell命令,用於QEMU的測試。
$wget http://wiki.qemu.org/download/linux-0.2.img.bz2
$bzip2 -d ./linux-0.2.img.bz2
3, 啓動gdb調試QEMU:
gdb --args qemu-system-x86_64 -enable-kvm -m 4096 -smp 4 linux-0.2.img
-smp指定處理器個數。
#二、參數解析用到的數據結構#
QEMU系統模擬的主函數位於vl.c文件,無論是qemu-system-x86_64還是qemu-system-ppc64,都是從vl.c中的main函數開始執行。下面先介紹main函數涉及到的一些數據結構。
###QEMU鏈表
QEMU的鏈表在include/qemu/queue.h文件中定義,分爲四種類型:
- 單鏈表(singly-linked list):單鏈表適用於大的數據集,並且很少有刪除節點或者移動節點的操作,也適用於實現後進先出的隊列。
- 鏈表(list):即雙向鏈表,除了頭節點之外每個節點都會同時指向前一個節點和後一個節點。
- 簡單隊列(simple queue):簡單隊列類似於單鏈表,只是多了一個指向鏈表尾的一個表頭,插入節點的時候不僅可以像單鏈表那樣將其插入表頭或者某節點之後,還可以插入到鏈表尾。
- 尾隊列(tail queue):類似於簡單隊列,但節點之間是雙向指向的。
這裏不一一介紹各種鏈表的用法,只通過NotifierList的定義來說明QEMU鏈表(list)的用法。在main函數的開頭定義的DisplayState結構體使用到了NotifiereList,NotifierList就用到了鏈表。
a. 表頭及節點的定義
定義表頭需要用到QLIST_HEAD,定義如下:
86 #define QLIST_HEAD(name, type) \
87 struct name { \
88 struct type *lh_first; /* first element */ \
89 }
NotifierList就採用了QLIST_HEAD來定義表頭:
27 typedef struct NotifierList
28 {
29 QLIST_HEAD(, Notifier) notifiers;
30 } NotifierList;
定義節點需要用到QLIST_ENTRY,定義如下:
94 #define QLIST_ENTRY(type) \
95 struct { \
96 struct type *le_next; /* next element */ \
97 struct type **le_prev; /* address of previous next element */ \
98 }
Notifier的節點定義如下:
21 struct Notifier
22 {
23 void (*notify)(Notifier *notifier, void *data);
24 QLIST_ENTRY(Notifier) node;
25 };
b. 初始化表頭
初始化表頭用到QLIST_INIT:
103 #define QLIST_INIT(head) do { \
104 (head)->lh_first = NULL; \
105 } while (/*CONSTCOND*/0)
初始化NotifierList就可以這樣進行:
19 void notifier_list_init(NotifierList *list)
20 {
21 QLIST_INIT(&list->notifiers);
22 }
c. 在表頭插入節點
將節點插入到表頭使用QLIST_INSERT_HEAD:
122 #define QLIST_INSERT_HEAD(head, elm, field) do { \
123 if (((elm)->field.le_next = (head)->lh_first) != NULL) \
124 (head)->lh_first->field.le_prev = &(elm)->field.le_next;\
125 (head)->lh_first = (elm); \
126 (elm)->field.le_prev = &(head)->lh_first; \
127 } while (/*CONSTCOND*/0)
插入Notifier到NotifierList:
24 void notifier_list_add(NotifierList *list, Notifier *notifier)
25 {
26 QLIST_INSERT_HEAD(&list->notifiers, notifier, node);
27 }
d. 遍歷節點
遍歷節點使用QLIST_FOREACH或者QLIST_FOREACH_SAFE,QLIST_FOREACH_SAFE是爲了防止遍歷過程中刪除了節點,從而導致le_next被釋放掉,中斷了遍歷。
147 #define QLIST_FOREACH(var, head, field) \
148 for ((var) = ((head)->lh_first); \
149 (var); \
150 (var) = ((var)->field.le_next))
151
152 #define QLIST_FOREACH_SAFE(var, head, field, next_var) \
153 for ((var) = ((head)->lh_first); \
154 (var) && ((next_var) = ((var)->field.le_next), 1); \
155 (var) = (next_var))
NotifierList在執行所有的回調函數時就用到了QLIST_FOREACH_SAFE:
34 void notifier_list_notify(NotifierList *list, void *data)
35 {
36 Notifier *notifier, *next;
37
38 QLIST_FOREACH_SAFE(notifier, &list->notifiers, node, next) {
39 notifier->notify(notifier, data);
40 }
41 }
###Error和QError
爲了方便的處理錯誤信息,QEMU定義了Error和QError兩個數據結構。Error在qobject/qerror.c中定義:
101 struct Error
102 {
103 char *msg;
104 ErrorClass err_class;
105 };
包含了錯誤消息字符串和枚舉類型的錯誤類別。錯誤類別有下面幾個:
139 typedef enum ErrorClass
140 {
141 ERROR_CLASS_GENERIC_ERROR = 0,
142 ERROR_CLASS_COMMAND_NOT_FOUND = 1,
143 ERROR_CLASS_DEVICE_ENCRYPTED = 2,
144 ERROR_CLASS_DEVICE_NOT_ACTIVE = 3,
145 ERROR_CLASS_DEVICE_NOT_FOUND = 4,
146 ERROR_CLASS_K_V_M_MISSING_CAP = 5,
147 ERROR_CLASS_MAX = 6,
148 } ErrorClass;
QEMU在util/error.c中定義了幾個函數來對Error進行操作:
error_set //根據給定的ErrorClass以及格式化字符串來給Error分配空間並賦值
error_set_errno //除了error_set的功能外,將指定errno的錯誤信息追加到格式化字符串的後面
error_copy //複製Error
error_is_set //判斷Error是否已經分配並設置
error_get_class //獲取Error的ErrorClass
error_get_pretty //獲取Error的msg
error_free //釋放Error及msg的空間
另外,QEMU定義了QError來處理更爲細緻的錯誤信息:
22 typedef struct QError {
23 QObject_HEAD;
24 Location loc;
25 char *err_msg;
26 ErrorClass err_class;
27 } QError;
QError可以通過一系列的宏來給err_msg及err_class賦值:
39 #define QERR_ADD_CLIENT_FAILED \
40 ERROR_CLASS_GENERIC_ERROR, "Could not add client"
41
42 #define QERR_AMBIGUOUS_PATH \
43 ERROR_CLASS_GENERIC_ERROR, "Path '%s' does not uniquely identify an object"
44
45 #define QERR_BAD_BUS_FOR_DEVICE \
46 ERROR_CLASS_GENERIC_ERROR, "Device '%s' can't go on a %s bus"
47
48 #define QERR_BASE_NOT_FOUND \
49 ERROR_CLASS_GENERIC_ERROR, "Base '%s' not found"
...
Location記錄了出錯的位置,定義如下:
20 typedef struct Location {
21 /* all members are private to qemu-error.c */
22 enum { LOC_NONE, LOC_CMDLINE, LOC_FILE } kind;
23 int num;
24 const void *ptr;
25 struct Location *prev;
26 } Location;
###GMainLoop
QEMU使用glib中的GMainLoop來實現IO多路複用,關於GMainLoop可以參考博客GMainLoop的實現原理和代碼模型。由於GMainLoop並非QEMU本身的代碼,本文就不重複贅述。
#三、QEMUOption、QemuOpt及QEMU參數解析
QEMU定義了QEMUOption來表示執行qemu-system-x86_64等命令時用到的選項。在vl.c中定義如下:
2123 typedef struct QEMUOption {
2124 const char *name; //選項名,如 -device, name的值就是device
2125 int flags; //標誌位,表示選項是否帶參數,可以是0,或者HAS_ARG(值爲0x0001)
2126 int index; //枚舉類型的值,如-device,該值就是QEMU_OPTION_device
2127 uint32_t arch_mask; // 選項支持架構的掩碼
2128 } QEMUOption;
vl.c中維護了一個QEMUOption數組qemu_options來存儲所有可用的選項,並利用qemu-options-wrapper.h和qemu-options.def來給該數組賦值。賦值語句如下:
2130 static const QEMUOption qemu_options[] = {
2131 { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL },
2132 #define QEMU_OPTIONS_GENERATE_OPTIONS
2133 #include "qemu-options-wrapper.h"
2134 { NULL },
2135 };
#define QEMU_OPTIONS_GENERATE_OPTIONS選擇qemu-options-wrapper.h的操作,qemu-options-wrapper.h可以進行三種操作:
QEMU_OPTIONS_GENERATE_ENUM: 利用qemu-options.def生成一個枚舉值列表,就是上面提到的QEMU_OPTION_device等
QEMU_OPTIONS_GENERATE_HELP: 利用qemu-options.def生成幫助信息並輸出到標準輸出
QEMU_OPTIONS_GENERATE_OPTIONS: 利用qemu-options.def生成一組選項列表
可以通過下面的方法來展開qemu-options-wrapper.h來查看上述操作的結果,以生成選項爲例。
- 在qemu-options-wrapper.h第一行寫入#define QEMU_OPTIONS_GENERATE_OPTIONS.
- 執行命令
gcc -E -o options.txt qemu-options-wrapper.h
- 查看文件options.txt即可
給qemu_options數組賦值後,QEMU就有了一個所有可用選項的集合。之後在vl.c中main函數的一個for循環根據這個集合開始解析命令行。for循環的框架大致如下:
1 for(;;) {
2 if (optind >= argc)
3 break;
4 if (argv[optind][0] != '-') {
5 hda_opts = drive_add(IF_DEFAULT, 0, argv[optind++], HD_OPTS);
6 } else {
7 const QEMUOption *popt;
8
9 popt = lookup_opt(argc, argv, &optarg, &optind);
10 if (!(popt->arch_mask & arch_type)) {
11 printf("Option %s not supported for this target\n", popt->name);
12 exit(1);
13 }
14 switch(popt->index) {
15 case QEMU_OPTION_M:
16 ......
17 case QEMU_OPTION_hda:
18 ......
19 case QEMU_OPTION_watchdog:
20 ......
21 default:
22 os_parse_cmd_args(popt->index, optarg);
23 }
24 }
25 }
QEMU會把argv中以'-'開頭的字符串當作選項,然後調用lookup_opt函數到qemu_options數組中查找該選項,如果查找到的選項中flags的值是HAS_ARG,lookup_opt也會將參數字符串賦值給optarg。找到選項和參數之後,QEMU便根據選項中的index枚舉值來執行不同的分支。
對於一些開關性質的選項,分支執行時僅僅是把相關的標誌位賦值而已,如:
3712 case QEMU_OPTION_old_param:
3713 old_param = 1;
3714 break;
也有一些選項沒有子選項,分支執行時就直接把optarg的值交給相關變量:
3822 case QEMU_OPTION_qtest:
3823 qtest_chrdev = optarg;
3824 break;
對於那些擁有子選項的選項,如"-drive if=none,id=DRIVE-ID",QEMU的處理會更爲複雜一些。它會調用qemu_opts_parse來解析子選項,如realtime選項的解析:
3852 case QEMU_OPTION_realtime:
3853 opts = qemu_opts_parse(qemu_find_opts("realtime"), optarg, 0);
3854 if (!opts) {
3855 exit(1);
3856 }
3857 configure_realtime(opts);
3858 break;
對子選項的解析涉及到4個數據結構:QemuOpt, QemuDesc, QemuOpts, QemuOptsList. 它們的關係如下圖所示:
QemuOpt存儲子選項,每個QemuOpt有一個QemuOptDesc來描述該子選項名字、類型、及幫助信息。兩個結構體定義如下:
32 struct QemuOpt {
33 const char *name; //子選項的名字
34 const char *str; //字符串值
35
36 const QemuOptDesc *desc;
37 union {
38 bool boolean; //布爾值
39 uint64_t uint; //數字或者大小
40 } value;
41
42 QemuOpts *opts;
43 QTAILQ_ENTRY(QemuOpt) next;
44 };
95 typedef struct QemuOptDesc {
96 const char *name;
97 enum QemuOptType type;
98 const char *help;
99 } QemuOptDesc;
子選項的類型可以是:
88 enum QemuOptType {
89 QEMU_OPT_STRING = 0, // 字符串
90 QEMU_OPT_BOOL, // 取值可以是on或者off
91 QEMU_OPT_NUMBER, // 數字
92 QEMU_OPT_SIZE, // 大小,可以有K, M, G, T等後綴
93 };
QEMU維護了一個QemuOptsList*的數組,在util/qemu-config.c中定義:
10 static QemuOptsList *vm_config_groups[32];
在main函數中由qemu_add_opts將各種QemuOptsList寫入到數組中:
2944 qemu_add_opts(&qemu_drive_opts);
2945 qemu_add_opts(&qemu_chardev_opts);
2946 qemu_add_opts(&qemu_device_opts);
2947 qemu_add_opts(&qemu_netdev_opts);
2948 qemu_add_opts(&qemu_net_opts);
2949 qemu_add_opts(&qemu_rtc_opts);
2950 qemu_add_opts(&qemu_global_opts);
2951 qemu_add_opts(&qemu_mon_opts);
2952 qemu_add_opts(&qemu_trace_opts);
2953 qemu_add_opts(&qemu_option_rom_opts);
2954 qemu_add_opts(&qemu_machine_opts);
2955 qemu_add_opts(&qemu_smp_opts);
2956 qemu_add_opts(&qemu_boot_opts);
2957 qemu_add_opts(&qemu_sandbox_opts);
2958 qemu_add_opts(&qemu_add_fd_opts);
2959 qemu_add_opts(&qemu_object_opts);
2960 qemu_add_opts(&qemu_tpmdev_opts);
2961 qemu_add_opts(&qemu_realtime_opts);
2962 qemu_add_opts(&qemu_msg_opts);
每個QemuOptsList存儲了大選項所支持的所有小選項,如-realtime大選項QemuOptsList的定義:
507 static QemuOptsList qemu_realtime_opts = {
508 .name = "realtime",
509 .head = QTAILQ_HEAD_INITIALIZER(qemu_realtime_opts.head),
510 .desc = {
511 {
512 .name = "mlock",
513 .type = QEMU_OPT_BOOL,
514 },
515 { /* end of list */ }
516 },
517 };
-realtime只支持1個子選項,且值爲bool類型,即只能是on或者off。
在調用qemu_opts_parse解析子選項之前,QEMU會調用qemu_find_opts("realtime"),把QemuOptsList *從qemu_add_opts中找出來,和optarg一起傳遞給qemu_opts_parse去解析。QEMU可能會多次使用同一個大選項來指定多個相同的設備,在這種情況下,需要用id來區分。QemuOpts結構體就表示同一id下所有的子選項,定義如下:
46 struct QemuOpts {
47 char *id;
48 QemuOptsList *list;
49 Location loc;
50 QTAILQ_HEAD(QemuOptHead, QemuOpt) head;
51 QTAILQ_ENTRY(QemuOpts) next;
52 };
其中list是同一個大選項下不同id的QemuOpts鏈表。