首页
社区
课程
招聘
[原创]qemu源码浅析之v0.1.6
2023-12-28 16:40 6201

[原创]qemu源码浅析之v0.1.6

2023-12-28 16:40
6201

前言

最近打算实现一个iot的fuzz,在此过程中遇到了许多问题,所以尝试通过阅读qemu源码来解决,不过现在的qemu已经是相当庞大的项目了,要想从当前版本的源码入手对于我这种小白来说过于困难,所以我会从历史版本入手,逐步分析v0.1.6,v0.10.6,v2.10.0这三个版本,本篇是对v0.1.6版本的源码分析。

源码分析

v0.1.6 这个版本是qemu相当早期的一个版本,许多后来我们熟知的功能比如tcg等,此时还没有引入,但对于我们了解qemu的结构,以及功能实现也有帮助,并且由于是早期代码,功能相对较少,读起来也相对容易,适合我这种新手阅读。

main函数

首先是参数解析的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
for(;;)
{
    if (optind >= argc)
        break;
    r = argv[optind];
    if (r[0] != '-')
        break;
    optind++;
    r++;
    if (!strcmp(r, "-"))
    {
        break;
    }
    else if (!strcmp(r, "d"))
    {
        loglevel = 1;
    }
    else if (!strcmp(r, "s"))
    {
        r = argv[optind++];
        x86_stack_size = strtol(r, (char **)&r, 0); /* 堆栈大小 */
        if (x86_stack_size <= 0)
            usage();
        if (*r == 'M')
            x86_stack_size *= 1024 * 1024;
        else if (*r == 'k' || *r == 'K')
            x86_stack_size *= 1024;
    }
    else if (!strcmp(r, "L"))
    {
        interp_prefix = argv[optind++];
    }
    else
    {
        usage();
    }
}

其中L用来指定解释器前缀,通常用来加载elf程序,比如 ld-linux.so.2

接着是

1
2
3
4
5
/* Zero out regs */
memset(regs, 0, sizeof(struct target_pt_regs));
 
/* Zero out image_info */
memset(info, 0, sizeof(struct image_info));

这两个函数用来清空regs和image_info的内存区域,其中target_pt_regs结构体定义了在x86架构下的通用寄存器组,用来模拟真实寄存器,而image_info结构体则用来描述ELF文件的信息定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct image_info {
    unsigned long   start_code; /* 代码段开始的位置 */
    unsigned long   end_code; /* 代码段终止的位置 */
    unsigned long   end_data;
    unsigned long   start_brk;
    unsigned long   brk;
    unsigned long   start_mmap; /* 开始映射的位置,虚拟地址 */
    unsigned long   mmap;
    unsigned long   rss; /* 程序实际消耗的页的数目 */
    unsigned long   start_stack; /* 堆栈开始的位置 */
    unsigned long   arg_start;
    unsigned long   arg_end;
    unsigned long   env_start;
    unsigned long   env_end;
    unsigned long   entry; /* 从这里开始执行代码 */
    int     personality;
};

接下来会调用:

1
2
3
4
5
if (elf_exec(filename, argv+optind, environ, regs, info) != 0)
   {
       printf("Error loading %s\n", filename);
       _exit(1);
   }

进入elf_exec这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int elf_exec(const char * filename, char ** argv, char ** envp,
             struct target_pt_regs * regs, struct image_info *infop)
{
    struct linux_binprm bprm;
    int retval;
    int i;
    /* p指向参数内存块的顶部 */
    bprm.p = X86_PAGE_SIZE*MAX_ARG_PAGES-sizeof(unsigned int); //指向高地址类似kernel mem
    for (i=0 ; i<MAX_ARG_PAGES ; i++)       /* clear page-table */
        bprm.page[i] = 0;
    retval = open(filename, O_RDONLY);
    if (retval == -1)
    {
        perror(filename);
        exit(-1);
        /* return retval; */
    }
    else
    {
        bprm.fd = retval;
    }
    bprm.filename = (char *)filename;
    bprm.sh_bang = 0;
    bprm.loader = 0;
    bprm.exec = 0;
    bprm.dont_iput = 0;
    bprm.argc = count(argv);
    bprm.envc = count(envp);

这里首先定义了一个struct linux_binprm类型的变量bprm,在linux系统中,这个结构体通常用来描述一个ELF的信息,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct linux_binprm
{
    char buf[128];
    unsigned long page[MAX_ARG_PAGES];
    unsigned long p;
    int sh_bang;
    int fd;
    int e_uid, e_gid;
    int argc; /* 参数个数 */
    int envc;
    /* 二进制文件的名称 */
    char * filename;        /* Name of binary */
    unsigned long loader;
    unsigned long exec;
    int dont_iput;          /* binfmt handler has put inode */
};

其中filename用来指向要执行的文件名,而p用来指向一个内存地址,在上面的源码中可以看到bprm.p指向了X86_PAGE_SIZE*MAX_ARG_PAGES-sizeof(unsigned int)这个地址,而这个地址是通过将一个页面的大小乘以最大页数得到的,是最高的地址,类似进程内存映像中的内核区,后续为了方便就把这部分称为内核区,接下来的部分就是对bprm结构体中的各种变量初始化。初始化完成后,会拷贝一些环境变量与参数到内核区,代码如下:

1
2
3
4
5
6
7
8
9
10
11
if(retval>=0)
{
    bprm.p = copy_strings(1, &bprm.filename, bprm.page, bprm.p);
    bprm.exec = bprm.p;
    bprm.p = copy_strings(bprm.envc,envp,bprm.page,bprm.p);
    bprm.p = copy_strings(bprm.argc,argv,bprm.page,bprm.p);
    if (!bprm.p)
    {
        retval = -E2BIG;
    }
}

这三个函数分别把要执行的文件名,参数个数,以及环境变量拷贝到bprm.p指向的区域,并返回新的bprm.p值。接下来便会加载elf文件:

1
2
3
4
if(retval>=0)
{
    retval = load_elf_binary(&bprm,regs,infop);
}

其中load_elf_binary函数部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int load_elf_binary(struct linux_binprm * bprm, struct target_pt_regs * regs,
                           struct image_info * info)
{
    struct elfhdr elf_ex;
    struct elfhdr interp_elf_ex;
    struct exec interp_ex;
    int interpreter_fd = -1; /* avoid warning */
    unsigned long load_addr, load_bias;
    int load_addr_set = 0;
    unsigned int interpreter_type = INTERPRETER_NONE;
    unsigned char ibcs2_interpreter;
    int i;
    void * mapped_addr;
    struct elf_phdr * elf_ppnt;
    struct elf_phdr *elf_phdata;
    unsigned long elf_bss, k, elf_brk;
    int retval;
    char * elf_interpreter;
    unsigned long elf_entry, interp_load_addr = 0;
    int status;
    unsigned long start_code, end_code, end_data;
    unsigned long elf_stack;
    char passed_fileno[6];
 
    ibcs2_interpreter = 0;
    status = 0;
    load_addr = 0;
    load_bias = 0;
    elf_ex = *((struct elfhdr *) bprm->buf);

这里定义了许多变量,先关注elf_ex这个变量,他是struct elfhdr类型的变量,查看交叉引用可以知道,该结构体是一个宏,实际上是struct elf32_hdr,该结构体定义了elf头部的常见的字段,接下来会检查elf文件是否合法,代码如下:

1
2
3
4
5
if (elf_ex.e_ident[0] != 0x7f ||
    strncmp(&elf_ex.e_ident[1], "ELF",3) != 0)
{
    return  -ENOEXEC;
}

elf_ex.e_indent是elf头部的魔数,首先检查elf_ex.e_indent[0]的位置是否为0x7f,其次检查elf_ex.e_indent[1-3]的位置是否为ELF,如果是则合法,否则返回-ENOEXEC,表示文件非法,不可执行。

接下来会对于整个ELF文件的信息,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
elf_phdata = (struct elf_phdr *)malloc(elf_ex.e_phentsize*elf_ex.e_phnum);
if (elf_phdata == NULL)
{
    return -ENOMEM;
}
/* 读取程序头表 */
retval = lseek(bprm->fd, elf_ex.e_phoff, SEEK_SET);
if(retval > 0)
{
    retval = read(bprm->fd, (char *) elf_phdata,
                  elf_ex.e_phentsize * elf_ex.e_phnum);
}
 
if (retval < 0)
{
    perror("load_elf_binary");
    exit(-1);
    free (elf_phdata);
    return -errno;
}

其中读入的大小为:elf_ex.e_phentsize*elf_ex.e_phnum,即elf中每个表项的大小乘以表项的数量,然后通过lseek函数将读写指针移动到e_phoff的位置,这个变量表示的是程序头表在ELF文件中的偏移,接着将程序头表读入到刚刚申请的空间中。

接下来便开始解析头表,首先初始化了如下变量,用于保存程序的各个段信息:

1
2
3
4
5
6
7
8
9
10
elf_ppnt = elf_phdata;
 
elf_bss = 0;
elf_brk = 0;
 
elf_stack = ~0UL;
elf_interpreter = NULL;
start_code = ~0UL;
end_code = 0;
end_data = 0;
  • elf_ppnt 指向程序头表中的当前表项。
  • elf_bss 表示 BSS 段的开始地址。
  • elf_brk 表示堆的末尾地址。
  • elf_stack 表示堆栈的末尾地址。
  • elf_interpreter 表示解释器的路径。
  • start_code 表示代码段的开始地址。
  • end_code 表示代码段的结束地址。
  • end_data 表示数据段的结束地址。

然后循环遍历各个表项的值,并逐个解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
for(i=0; i < elf_ex.e_phnum; i++)
    {
        if (elf_ppnt->p_type == PT_INTERP)
        {
            if ( elf_interpreter != NULL )
            {
                free (elf_phdata);
                free(elf_interpreter);
                close(bprm->fd);
                return -EINVAL;
            }
 
            /* This is the program interpreter used for
             * shared libraries - for now assume that this
             * is an a.out format binary
             */
 
            elf_interpreter = (char *)malloc(elf_ppnt->p_filesz);
 
            if (elf_interpreter == NULL)
            {
                free (elf_phdata);
                close(bprm->fd);
                return -ENOMEM;
            }
            /* 读取Segment的信息 */
            retval = lseek(bprm->fd, elf_ppnt->p_offset, SEEK_SET);
            if(retval >= 0)
            {
                retval = read(bprm->fd, elf_interpreter, elf_ppnt->p_filesz);
            }
            if(retval < 0)
            {
                perror("load_elf_binary2");
                exit(-1);
            }
 
            /* If the program interpreter is one of these two,
               then assume an iBCS2 image. Otherwise assume
               a native linux image. */
 
            /* JRP - Need to add X86 lib dir stuff here... */
            /* 共享库的路径 */
            if (strcmp(elf_interpreter,"/usr/lib/libc.so.1") == 0 ||
                strcmp(elf_interpreter,"/usr/lib/ld.so.1") == 0)
            {
                ibcs2_interpreter = 1;
            }
 
#if 0
            printf("Using ELF interpreter %s\n", elf_interpreter);
#endif
            if (retval >= 0)
            {
                /* 加载共享库 */
                retval = open(path(elf_interpreter), O_RDONLY);
                if(retval >= 0)
                {
                    interpreter_fd = retval;
                }
                else
                {
                    perror(elf_interpreter);
                    exit(-1);
                    /* retval = -errno; */
                }
            }
 
            if (retval >= 0)
            {
                retval = lseek(interpreter_fd, 0, SEEK_SET);
                if(retval >= 0)
                {
                    retval = read(interpreter_fd,bprm->buf,128);
                }
            }
            if (retval >= 0)
            {
                interp_ex = *((struct exec *) bprm->buf); /* aout exec-header */
                interp_elf_ex=*((struct elfhdr *) bprm->buf); /* elf exec-header */
            }
            if (retval < 0)
            {
                perror("load_elf_binary3");
                exit(-1);
                free (elf_phdata);
                free(elf_interpreter);
                close(bprm->fd);
                return retval;
            }
        }
        elf_ppnt++;
    }

这段代码将加载ld与其他段到内存中,为执行做准备,后面会对各个段做初始化,需要注意的是堆的结束地址也就是info->brk的值在这之中被确定,至于为什么brk存放的是堆的结束地址,个人觉得是因为当需要分配堆内存时,可以通过该变量配合检查是否还有足够的空间进行分配。

这段代码完成后将返回elf_exec函数继续执行:

1
2
3
4
5
6
7
if(retval>=0)
  {
      /* success.  Initialize important registers */
      regs->esp = infop->start_stack;
      regs->eip = infop->entry;
      return retval;
  }

当上述步骤成功完成后,接下来会对栈顶指针和eip做初始化,值为栈的起始地址以及elf文件的加载基地址,并返回main函数执行 syscall_init();signal_init();这两个函数,下面分别分析这两个函数。

  • syscall_init函数内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    void syscall_init(void)
    {
    #define STRUCT(name, list...) thunk_register_struct(STRUCT_ ## name, #name, struct_ ## name ## _def);
    #define STRUCT_SPECIAL(name) thunk_register_struct_direct(STRUCT_ ## name, #name, &struct_ ## name ## _def);
    #include "syscall_types.h"
    #undef STRUCT
    #undef STRUCT_SPECIAL
    }

    这里使用了thunck机制向内核中注册结构体,thunck机制提供了允许在用户态下访问内核的结构体或者函数,允许用户态的代码与内核态的代码进行通信等功能。

    例如当我们写出如下代码:

    1
    2
    3
    4
    5
    STRUCT(
        test,
        int a;
        int b;
    )

    则会通过该宏定义转换为如下代码:

    1
    thunck_register_struct(STRUCT_test, "test", struct_test_def);

    这段代码将向内核注册名为test的结构体,其中第三个参数为test结构体的定义。

  • signal_init函数代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    void signal_init(void)
    {
        struct sigaction act;
        int i;
     
        /* set all host signal handlers. ALL signals are blocked during
           the handlers to serialize them. */
        /* 注册所有的信号处理函数 */
        sigfillset(&act.sa_mask);
        act.sa_flags = SA_SIGINFO;
        act.sa_sigaction = host_signal_handler;
        for(i = 1; i < NSIG; i++) {
        sigaction(i, &act, NULL);
        }
         
        memset(sigact_table, 0, sizeof(sigact_table));
     
        first_free = &sigqueue_table[0];
        for(i = 0; i < MAX_SIGQUEUE_SIZE - 1; i++)
            sigqueue_table[i].next = &sigqueue_table[i + 1];
        sigqueue_table[MAX_SIGQUEUE_SIZE - 1].next = NULL;
    }

    这段代码注册了信号处理的队列,需要注意的是使用sigfillset将信号加入act.sa_mask中,是为了在处理当前信号是不被中断,若处理当前信号时又来了新的信号,将会加入队列sigqueue_table中,稍后处理。

执行完上述两个函数后,将会继续执行cpu_x86_init()这个函数用于设置基本的运行环境,该函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CPUX86State *cpu_x86_init(void)
{
    CPUX86State *env;
    int i;
    static int inited;
 
    cpu_x86_tblocks_init();
 
    env = malloc(sizeof(CPUX86State));
    if (!env)
        return NULL;
    memset(env, 0, sizeof(CPUX86State));
    /* basic FPU init */
    for(i = 0;i < 8; i++)
        env->fptags[i] = 1;
    env->fpuc = 0x37f;
    /* flags setup : we activate the IRQs by default as in user mode */
    env->eflags = 0x2 | IF_MASK;
 
    /* init various static tables */
    if (!inited) {
        inited = 1;
        optimize_flags_init();
    }
    return env;
}

接下来将执行:

1
2
3
4
/* build Task State */
memset(ts, 0, sizeof(TaskState));
env->opaque = ts;
ts->used = 1;

其中TaskState的定义如下:

1
2
3
4
5
6
7
typedef struct TaskState {
    struct TaskState *next;
    struct target_vm86plus_struct *target_v86;
    struct vm86_saved_state vm86_saved_regs;
    int used; /* non zero if used */
    uint8_t stack[0];
} __attribute__((aligned(16))) TaskState;

这是一个关于任务的结构体,在qemu中,可能需要并发执行多个任务。每个任务可以有自己的 CPU 寄存器状态、栈空间等。TaskState 结构体可以用于管理这些任务的状态,并在任务切换时保存和恢复寄存器状态,用next指针将任务链接起来,taget_vm86plus_struct结构体中嵌套的定义太多,这里只挑一些关键点说明,这个结构体中定义了x86架构下的通用寄存器组,以及一些关于中断的结构体定义。

将ts的值赋值给env-opaque变量,这个变量指向了用户的数据。

接下来将对各种寄存器以及段进行初始化,然后将进入cpu_loop开始模拟执行程序。

cpu_loop函数

cpu_loop函数用于模拟cpu执行的过程,首先会调用cpu_x86_exec函数进行执行,该函数首先定义了如下变量:

1
2
3
4
5
6
7
8
9
10
11
int saved_T0, saved_T1, saved_A0; /* 默认仅仅使用了3个寄存器 */
CPUX86State *saved_env;    int code_gen_size, ret;
void (*gen_func)(void);
TranslationBlock *tb, **ptb;
uint8_t *tc_ptr, *cs_base, *pc;
unsigned int flags;
int code_gen_size, ret;
void (*gen_func)(void);
TranslationBlock *tb, **ptb;
uint8_t *tc_ptr, *cs_base, *pc;
unsigned int flags;

其中*tb用于指向某一个特定的翻译块,而**ptb这个二重指针可以认为是一个指针数组,用来指向所有的翻译块。

这里重点关注TranslationBlock结构体,其定义如下:

1
2
3
4
5
6
7
typedef struct TranslationBlock {
    unsigned long pc;   /* simulated PC corresponding to this block (EIP + CS base) */
    unsigned long cs_base; /* CS base for this block */
    unsigned int flags; /* flags defining in which context the code was generated */
    uint8_t *tc_ptr;    /* pointer to the translated code */
    struct TranslationBlock *hash_next; /* next matching block */
} TranslationBlock;

pc用于指向下一个要执行的地址,是对程序计数器的模拟,采用了基址寻址的方式,值为EIP + CS BASEEIP为寄存器的值指向要执行的语句,并加上cs段的基址来得到pc的值。

flags用于表示该翻译块的属性。

tc_ptr用于指向翻译得到的代码,即当前基本块经过翻译后得到的机器指令。

hash_next指向下一个具有相同hash值的TranslationBlock结构体,hash_next的作用类似于cache,若当前指令已经被翻译过,则直接找到存放该指令的TranslationBlock,而不需要再次翻译,从而提高速度。

该结构体的具体作用就是用来存放在执行过程中,经过翻译得到的机器码。

接下来将执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* put eflags in CPU temporary format */
    CC_SRC = env->eflags & (CC_O | CC_S | CC_Z | CC_A | CC_P | CC_C);
    DF = 1 - (2 * ((env->eflags >> 10) & 1));
    CC_OP = CC_OP_EFLAGS;
    env->eflags &= ~(DF_MASK | CC_O | CC_S | CC_Z | CC_A | CC_P | CC_C);
    env->interrupt_request = 0; /* 清除中断请求 */
 
    /* prepare setjmp context for exception handling */
    /* 准备setjmp上下文,为异常处理做准备 */
    if (setjmp(env->jmp_env) == 0) {
        for(;;) {
            if (env->interrupt_request) {
                raise_exception(EXCP_INTERRUPT);
            }
#ifdef DEBUG_EXEC
            if (loglevel) {
                cpu_x86_dump_state(logfile);
            }
#endif
            /* we compute the CPU state. We assume it will not
               change during the whole generated block. */
            flags = env->seg_cache[R_CS].seg_32bit << GEN_FLAG_CODE32_SHIFT;
            flags |= env->seg_cache[R_SS].seg_32bit << GEN_FLAG_SS32_SHIFT;
            flags |= (((unsigned long)env->seg_cache[R_DS].base |
                       (unsigned long)env->seg_cache[R_ES].base |
                       (unsigned long)env->seg_cache[R_SS].base) != 0) <<
                GEN_FLAG_ADDSEG_SHIFT;
            flags |= (env->eflags & VM_MASK) >> (17 - GEN_FLAG_VM_SHIFT);
            cs_base = env->seg_cache[R_CS].base; /* 代码段的起始地址 */
            pc = cs_base + env->eip;
            tb = tb_find(&ptb, (unsigned long)pc, (unsigned long)cs_base,
                         flags);

这段代码中,先是将env->interrupt_request设置为0,表示开始执行时,无任何中断请求,接下来的一大段移位操作可以不管,但我们需要注意,此时确定了cs_base的值为env->seg_cache[R_CS].baseseg_cache数组存放了来源于LDT或者GDT中的段信息,从其中读出CS的地址,并赋值给变量cs_base,从而计算出PC寄存器的值,从而得到了第一条指令的地址。

接下来便会进入tb_find函数用于寻找当前翻译块是否被翻译过了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline TranslationBlock *tb_find(TranslationBlock ***pptb,
                                        unsigned long pc,
                                        unsigned long cs_base,
                                        unsigned int flags)
{
    TranslationBlock **ptb, *tb;
    unsigned int h;
 
    h = pc & (CODE_GEN_HASH_SIZE - 1);
    ptb = &tb_hash[h];
    for(;;) {
        tb = *ptb;
        if (!tb)
            break;
        if (tb->pc == pc && tb->cs_base == cs_base && tb->flags == flags)
            return tb;
        ptb = &tb->hash_next;
    }
    *pptb = ptb;
    return NULL;
}

在这里我们可以看到hash值是如何计算的,通过将hash表的大小-1和pc寄存器的值相遇得到hash值。

若当前代码以及被翻译过,则在对应的tb_hash[h]中就能找到对应项,然后通过循环检查其pc、cs_base、flags等值是否匹配,如果匹配则返回该翻译块,而不需要再次翻译。

返回以后,检查tb的值,若为0,则说明缓存中并未找到翻译块,说明未被翻译过,则开始翻译,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (!tb) {
    cpu_lock();
    tc_ptr = code_gen_ptr;
    ret = cpu_x86_gen_code(code_gen_ptr, CODE_GEN_MAX_SIZE,
                           &code_gen_size, pc, cs_base, flags);
    /* if invalid instruction, signal it */
    if (ret != 0) {
        cpu_unlock();
        raise_exception(EXCP06_ILLOP);
    }
    tb = tb_alloc();
    *ptb = tb;
    tb->pc = (unsigned long)pc;
    tb->cs_base = (unsigned long)cs_base;
    tb->flags = flags;
    tb->tc_ptr = tc_ptr;
    tb->hash_next = NULL;
    code_gen_ptr = (void *)(((unsigned long)code_gen_ptr + code_gen_size + CODE_GEN_ALIGN - 1) & ~(CODE_GEN_ALIGN - 1));
    cpu_unlock();
}

首先对通过TestAndSet方法,模拟硬件上锁的过程,然后将code_gen_ptr的值赋给tc_ptrcode_gen_ptr用于指向当前正在生成的代码,而tc_ptr用于指向翻译后的代码。

然后将调用cpu_x86_gen_code来生成代码,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
dc->code32 = (flags >> GEN_FLAG_CODE32_SHIFT) & 1; /* 代码段 */
dc->ss32 = (flags >> GEN_FLAG_SS32_SHIFT) & 1; /* 栈段 */
dc->addseg = (flags >> GEN_FLAG_ADDSEG_SHIFT) & 1;
dc->f_st = (flags >> GEN_FLAG_ST_SHIFT) & 7;
dc->vm86 = (flags >> GEN_FLAG_VM_SHIFT) & 1;
dc->cc_op = CC_OP_DYNAMIC; /* 动态获取flags */
dc->cs_base = cs_base;
gen_opc_ptr = gen_opc_buf;
gen_opc_end = gen_opc_buf + OPC_MAX_SIZE;
gen_opparam_ptr = gen_opparam_buf;
 
dc->is_jmp = 0;
pc_ptr = pc_start;

这里首先通过对flags的移位操作来设置变量dc的一些字段,然后设置要翻译的指令的起始与结束,并将传入的参数赋值给变量,其中dc的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct DisasContext {
    /* current insn context */
    int override; /* -1 if no override */
    int prefix;
    int aflag, dflag;
    uint8_t *pc; /* pc = eip + cs_base */
    int is_jmp; /* 1 = means jump (stop translation), 2 means CPU
                   static state change (stop translation) */
    /* current block context */
    uint8_t *cs_base; /* base of CS segment */
    int code32; /* 32 bit code segment */
    int ss32;   /* 32 bit stack segment */
    int cc_op;  /* current CC operation */
    int addseg; /* non zero if either DS/ES/SS have a non zero base */
    int f_st;   /* currently unused */
    int vm86;   /* vm86 mode */
} DisasContext;

该结构体各字段意义已经有了注释,就不在此过多阐述,接下来便会调用disas_insn来进行反汇编,对于这部分不做过多分析,但需要注意的是,在disas_insn中,指令反汇编后将会直接执行,并在最后返回时,返回异常的编号,并在cpu_loop中进行异常处理,对于系统调用的处理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
else
{
    if (pc[0] == 0xcd && pc[1] == 0x80)
    {
        /* syscall */
        env->eip += 2;
        env->regs[R_EAX] = do_syscall(env,
                                      env->regs[R_EAX],
                                      env->regs[R_EBX],
                                      env->regs[R_ECX],
                                      env->regs[R_EDX],
                                      env->regs[R_ESI],
                                      env->regs[R_EDI],
                                      env->regs[R_EBP]);
    }
    else
    {
        /* XXX: more precise info */
        info.si_signo = SIGSEGV;
        info.si_errno = 0;
        info.si_code = 0;
        info._sifields._sigfault._addr = 0;
        queue_signal(info.si_signo, &info);
    }
}

这里首先检查pc的值是否为0xcd80,这是int 0x80的机器码,如果是,则说明进行了系统调用,并将寄存器的值传入到do_syscall函数中,进行处理,并把返回值保存到EAX寄存器中,do_syscall部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
long do_syscall(void *cpu_env, int num, long arg1, long arg2, long arg3,
                long arg4, long arg5, long arg6)
{
    long ret;
    struct stat st;
    struct kernel_statfs *stfs;
     
#ifdef DEBUG
    gemu_log("syscall %d\n", num);
#endif
    switch(num) {
    case TARGET_NR_exit:
#ifdef HAVE_GPROF
        _mcleanup();
#endif
        /* XXX: should free thread stack and CPU env */
        _exit(arg1);
        ret = 0; /* avoid warning */
        break;
    case TARGET_NR_read:
        ret = get_errno(read(arg1, (void *)arg2, arg3));
        break;
    case TARGET_NR_write:
        ret = get_errno(write(arg1, (void *)arg2, arg3));
        break;
    case TARGET_NR_open:
        ret = get_errno(open(path((const char *)arg1), arg2, arg3));
        break;

这里的num就是系统调用号,我们可以看到,在qemu早期的版本中,对于openreadwrite等系统调用,都是直接执行相关函数,从而实现对系统调用的模拟。

结语

通过学习qemu的源码,让我对qemu的实现有了一个大概的认识,虽然分析的比较浅显以及跳过了许多代码,但也希望能给和我一样的初学者一些帮助。

本人水平有限,如果有任何问题欢迎师傅们指出。

参考链接:

https://github.com/lishuhuakai/qemu_reading


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞7
打赏
分享
最新回复 (3)
雪    币: 577
活跃值: (47816)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小旺不正经 2023-12-29 08:24
2
0
顶一下
雪    币: 19431
活跃值: (29092)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-12-29 11:12
3
1
感谢分享
雪    币: 9
活跃值: (6842)
能力值: ( LV13,RANK:383 )
在线值:
发帖
回帖
粉丝
winmt 8 2023-12-29 17:09
4
0
太强了
游客
登录 | 注册 方可回帖
返回