其他
Qemu源码浅析之v0.1.6
一
前言
二
源码分析
qemu
相当早期的一个版本,许多后来我们熟知的功能比如tcg
等,此时还没有引入,但对于我们了解qemu
的结构,以及功能实现也有帮助,并且由于是早期代码,功能相对较少,读起来也相对容易,适合我这种新手阅读。main函数
{
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();
}
}
ld-linux.so.2
。memset(regs, 0, sizeof(struct target_pt_regs));
/* Zero out image_info */
memset(info, 0, sizeof(struct image_info));
target_pt_regs
结构体定义了在x86架构下的通用寄存器组,用来模拟真实寄存器,而image_info
结构体则用来描述ELF文件的信息定义如下: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;
};
{
printf("Error loading %s\n", filename);
_exit(1);
}
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的信息,定义如下:{
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 */
};
bprm.p
指向了X86_PAGE_SIZE*MAX_ARG_PAGES-sizeof(unsigned int)
这个地址,而这个地址是通过将一个页面的大小乘以最大页数得到的,是最高的地址,类似进程内存映像中的内核区,后续为了方便就把这部分称为内核区,接下来的部分就是对bprm结构体中的各种变量初始化。初始化完成后,会拷贝一些环境变量与参数到内核区,代码如下:{
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文件:{
retval = load_elf_binary(&bprm,regs,infop);
}
load_elf_binary
函数部分代码如下: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文件是否合法,代码如下: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,表示文件非法,不可执行。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文件中的偏移,接着将程序头表读入到刚刚申请的空间中。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
表示数据段的结束地址。{
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++;
}
info->brk
的值在这之中被确定,至于为什么brk存放的是堆的结束地址,个人觉得是因为当需要分配堆内存时,可以通过该变量配合检查是否还有足够的空间进行分配。elf_exec
函数继续执行:{
/* success. Initialize important registers */
regs->esp = infop->start_stack;
regs->eip = infop->entry;
return retval;
}
syscall_init();
与signal_init();
这两个函数,下面分别分析这两个函数。syscall_init
函数内容如下: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机制提供了允许在用户态下访问内核的结构体或者函数,允许用户态的代码与内核态的代码进行通信等功能。
例如当我们写出如下代码:
STRUCT(
test,
int a;
int b;
)
thunck_register_struct(STRUCT_test, "test", struct_test_def);
signal_init
函数代码如下: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;
}
cpu_x86_init()
这个函数用于设置基本的运行环境,该函数代码如下:{
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;
}
memset(ts, 0, sizeof(TaskState));
env->opaque = ts;
ts->used = 1;
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;
TaskState
结构体可以用于管理这些任务的状态,并在任务切换时保存和恢复寄存器状态,用next指针将任务链接起来,taget_vm86plus_struct
结构体中嵌套的定义太多,这里只挑一些关键点说明,这个结构体中定义了x86架构下的通用寄存器组,以及一些关于中断的结构体定义。env-opaque
变量,这个变量指向了用户的数据。cpu_loop
开始模拟执行程序。cpu_loop函数
cpu_loop
函数用于模拟cpu执行的过程,首先会调用cpu_x86_exec
函数进行执行,该函数首先定义了如下变量: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
结构体,其定义如下: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;
EIP + CS BASE
,EIP
为寄存器的值指向要执行的语句,并加上cs段的基址来得到pc的值。TranslationBlock
结构体,hash_next的作用类似于cache,若当前指令已经被翻译过,则直接找到存放该指令的TranslationBlock
,而不需要再次翻译,从而提高速度。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].base
,seg_cache
数组存放了来源于LDT
或者GDT
中的段信息,从其中读出CS的地址,并赋值给变量cs_base
,从而计算出PC寄存器的值,从而得到了第一条指令的地址。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;
}
tb_hash[h]
中就能找到对应项,然后通过循环检查其pc、cs_base、flags等值是否匹配,如果匹配则返回该翻译块,而不需要再次翻译。tb
的值,若为0,则说明缓存中并未找到翻译块,说明未被翻译过,则开始翻译,代码如下: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_ptr
,code_gen_ptr
用于指向当前正在生成的代码,而tc_ptr
用于指向翻译后的代码。cpu_x86_gen_code
来生成代码,具体内容如下: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;
/* 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
中进行异常处理,对于系统调用的处理代码如下:{
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);
}
}
0xcd80
,这是int 0x80
的机器码,如果是,则说明进行了系统调用,并将寄存器的值传入到do_syscall
函数中,进行处理,并把返回值保存到EAX
寄存器中,do_syscall
部分代码如下: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;
open
、read
、write
等系统调用,都是直接执行相关函数,从而实现对系统调用的模拟。三
结语
看雪ID:h1J4cker
https://bbs.kanxue.com/user-home-951494.htm
# 往期推荐
1、堆利用学习:the house of einherjar
6、以 corCTF 2023 sysruption 学习 sysret bug 的利用
球分享
球点赞
球在看
点击阅读原文查看更多