Unicorn-CPU模拟框架数据类型及API分析与示例(四)

Author Avatar
kabeor 2月 13, 2020

Unicorn-CPU模拟框架数据类型及API分析与示例(四)

0x2 API分析

uc_hook_add

uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void *callback,
void *user_data, uint64_t begin, uint64_t end, ...);

注册hook事件的回调,当hook事件被触发将会进行回调。

@uc: uc_open() 返回的句柄
@hh: 注册hook得到的句柄. uc_hook_del() 中使用
@type: hook 类型
@callback: 当指令被命中时要运行的回调
@user_data: 用户自定义数据. 将被传递给回调函数的最后一个参数 @user_data
@begin: 回调生效区域的起始地址(包括)
@end: 回调生效区域的结束地址(包括)
注意 1: 只有回调的地址在[@begin, @end]中才会调用回调
注意 2: 如果 @begin > @end, 每当触发此hook类型时都会调用回调
@...: 变量参数 (取决于 @type)
注意: 如果 @type = UC_HOOK_INSN, 这里是指令ID (如: UC_X86_INS_OUT)

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void *callback,
void *user_data, uint64_t begin, uint64_t end, ...)
{
int ret = UC_ERR_OK;
int i = 0;

struct hook *hook = calloc(1, sizeof(struct hook));
if (hook == NULL) {
return UC_ERR_NOMEM;
}

hook->begin = begin;
hook->end = end;
hook->type = type;
hook->callback = callback;
hook->user_data = user_data;
hook->refs = 0;
*hh = (uc_hook)hook;

// UC_HOOK_INSN 有一个额外参数:指令ID
if (type & UC_HOOK_INSN) {
va_list valist;

va_start(valist, end);
hook->insn = va_arg(valist, int);
va_end(valist);

if (uc->insn_hook_validate) {
if (! uc->insn_hook_validate(hook->insn)) {
free(hook);
return UC_ERR_HOOK;
}
}

if (uc->hook_insert) {
if (list_insert(&uc->hook[UC_HOOK_INSN_IDX], hook) == NULL) {
free(hook);
return UC_ERR_NOMEM;
}
} else {
if (list_append(&uc->hook[UC_HOOK_INSN_IDX], hook) == NULL) {
free(hook);
return UC_ERR_NOMEM;
}
}

hook->refs++;
return UC_ERR_OK;
}

while ((type >> i) > 0) {
if ((type >> i) & 1) {
if (i < UC_HOOK_MAX) {
if (uc->hook_insert) {
if (list_insert(&uc->hook[i], hook) == NULL) {
if (hook->refs == 0) {
free(hook);
}
return UC_ERR_NOMEM;
}
} else {
if (list_append(&uc->hook[i], hook) == NULL) {
if (hook->refs == 0) {
free(hook);
}
return UC_ERR_NOMEM;
}
}
hook->refs++;
}
}
i++;
}

if (hook->refs == 0) {
free(hook);
}

return ret;
}

使用示例:

#include <iostream>
#include <string>
#include "unicorn/unicorn.h"
using namespace std;

int syscall_abi[] = {
UC_X86_REG_RAX, UC_X86_REG_RDI, UC_X86_REG_RSI, UC_X86_REG_RDX,
UC_X86_REG_R10, UC_X86_REG_R8, UC_X86_REG_R9
};

uint64_t vals[7] = { 200, 10, 11, 12, 13, 14, 15 };

void* ptrs[7];

void uc_perror(const char* func, uc_err err)
{
fprintf(stderr, "Error in %s(): %s\n", func, uc_strerror(err));
}

#define BASE 0x10000

// mov rax, 100; mov rdi, 1; mov rsi, 2; mov rdx, 3; mov r10, 4; mov r8, 5; mov r9, 6; syscall
#define CODE "\x48\xc7\xc0\x64\x00\x00\x00\x48\xc7\xc7\x01\x00\x00\x00\x48\xc7\xc6\x02\x00\x00\x00\x48\xc7\xc2\x03\x00\x00\x00\x49\xc7\xc2\x04\x00\x00\x00\x49\xc7\xc0\x05\x00\x00\x00\x49\xc7\xc1\x06\x00\x00\x00\x0f\x05"

void hook_syscall(uc_engine* uc, void* user_data)
{
int i;

uc_reg_read_batch(uc, syscall_abi, ptrs, 7);

printf("syscall: {");

for (i = 0; i < 7; i++) {
if (i != 0) printf(", ");
printf("%" PRIu64, vals[i]);
}

printf("}\n");
}

void hook_code(uc_engine* uc, uint64_t addr, uint32_t size, void* user_data)
{
printf("HOOK_CODE: 0x%" PRIx64 ", 0x%x\n", addr, size);
}

int main()
{
int i;
uc_hook sys_hook;
uc_err err;
uc_engine* uc;

for (i = 0; i < 7; i++) {
ptrs[i] = &vals[i];
}

if ((err = uc_open(UC_ARCH_X86, UC_MODE_64, &uc))) {
uc_perror("uc_open", err);
return 1;
}

printf("reg_write_batch({200, 10, 11, 12, 13, 14, 15})\n");
if ((err = uc_reg_write_batch(uc, syscall_abi, ptrs, 7))) {
uc_perror("uc_reg_write_batch", err);
return 1;
}

memset(vals, 0, sizeof(vals));
if ((err = uc_reg_read_batch(uc, syscall_abi, ptrs, 7))) {
uc_perror("uc_reg_read_batch", err);
return 1;
}

printf("reg_read_batch = {");

for (i = 0; i < 7; i++) {
if (i != 0) printf(", ");
printf("%" PRIu64, vals[i]);
}

printf("}\n");

// syscall
printf("\n");
printf("running syscall shellcode\n");

if ((err = uc_hook_add(uc, &sys_hook, UC_HOOK_CODE, hook_syscall, NULL, 1, 0))) {
uc_perror("uc_hook_add", err);
return 1;
}

if ((err = uc_mem_map(uc, BASE, 0x1000, UC_PROT_ALL))) {
uc_perror("uc_mem_map", err);
return 1;
}

if ((err = uc_mem_write(uc, BASE, CODE, sizeof(CODE) - 1))) {
uc_perror("uc_mem_write", err);
return 1;
}

if ((err = uc_emu_start(uc, BASE, BASE + sizeof(CODE) - 1, 0, 0))) {
uc_perror("uc_emu_start", err);
return 1;
}

return 0;
}

输出

image.png

对每条指令都进行hook

uc_hook_del

uc_err uc_hook_del(uc_engine *uc, uc_hook hh);

删除一个已注册的hook事件

@uc: uc_open() 返回的句柄
@hh: uc_hook_add() 返回的句柄

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_hook_del(uc_engine *uc, uc_hook hh)
{
int i;
struct hook *hook = (struct hook *)hh;

for (i = 0; i < UC_HOOK_MAX; i++) {
if (list_remove(&uc->hook[i], (void *)hook)) {
if (--hook->refs == 0) {
free(hook);
break;
}
}
}
return UC_ERR_OK;
}

使用示例:

if ((err = uc_hook_add(uc, &sys_hook, UC_HOOK_CODE, hook_syscall, NULL, 1, 0))) {
uc_perror("uc_hook_add", err);
return 1;
}

if ((err = uc_hook_del(uc, &sys_hook))) {
uc_perror("uc_hook_del", err);
return 1;
}

uc_mem_map

uc_err uc_mem_map(uc_engine *uc, uint64_t address, size_t size, uint32_t perms);

为模拟映射一块内存。

@uc: uc_open() 返回的句柄
@address: 要映射到的新内存区域的起始地址。这个地址必须与4KB对齐,否则将返回UC_ERR_ARG错误。
@size: 要映射到的新内存区域的大小。这个大小必须是4KB的倍数,否则将返回UC_ERR_ARG错误。
@perms: 新映射区域的权限。参数必须是UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC或这些的组合,否则返回UC_ERR_ARG错误。

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_mem_map(uc_engine *uc, uint64_t address, size_t size, uint32_t perms)
{
uc_err res;

if (uc->mem_redirect) {
address = uc->mem_redirect(address);
}

res = mem_map_check(uc, address, size, perms); //内存安全检查
if (res)
return res;

return mem_map(uc, address, size, perms, uc->memory_map(uc, address, size, perms));
}

使用示例同uc_hook_add。

uc_mem_map_ptr

uc_err uc_mem_map_ptr(uc_engine *uc, uint64_t address, size_t size, uint32_t perms, void *ptr);

在模拟中映射现有的主机内存。

@uc: uc_open() 返回的句柄
@address: 要映射到的新内存区域的起始地址。这个地址必须与4KB对齐,否则将返回UC_ERR_ARG错误。
@size: 要映射到的新内存区域的大小。这个大小必须是4KB的倍数,否则将返回UC_ERR_ARG错误。
@perms: 新映射区域的权限。参数必须是UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC或这些的组合,否则返回UC_ERR_ARG错误。
@ptr: 指向支持新映射内存的主机内存的指针。映射的主机内存的大小应该与size的大小相同或更大,并且至少使用PROT_READ | PROT_WRITE进行映射,否则不定义映射。

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_mem_map_ptr(uc_engine *uc, uint64_t address, size_t size, uint32_t perms, void *ptr)
{
uc_err res;

if (ptr == NULL)
return UC_ERR_ARG;

if (uc->mem_redirect) {
address = uc->mem_redirect(address);
}

res = mem_map_check(uc, address, size, perms); //内存安全检查
if (res)
return res;

return mem_map(uc, address, size, UC_PROT_ALL, uc->memory_map_ptr(uc, address, size, perms, ptr));
}

使用示例同uc_mem_map

uc_mem_unmap

uc_err uc_mem_unmap(uc_engine *uc, uint64_t address, size_t size);

取消对模拟内存区域的映射

@uc: uc_open() 返回的句柄
@address: 要映射到的新内存区域的起始地址。这个地址必须与4KB对齐,否则将返回UC_ERR_ARG错误。
@size: 要映射到的新内存区域的大小。这个大小必须是4KB的倍数,否则将返回UC_ERR_ARG错误。

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_mem_unmap(struct uc_struct *uc, uint64_t address, size_t size)
{
MemoryRegion *mr;
uint64_t addr;
size_t count, len;

if (size == 0)
// 没有要取消映射的区域
return UC_ERR_OK;

// 地址必须对齐到 uc->target_page_size
if ((address & uc->target_page_align) != 0)
return UC_ERR_ARG;

// 大小必须是 uc->target_page_size 的倍数
if ((size & uc->target_page_align) != 0)
return UC_ERR_ARG;

if (uc->mem_redirect) {
address = uc->mem_redirect(address);
}

// 检查用户请求的整个块是否被映射
if (!check_mem_area(uc, address, size))
return UC_ERR_NOMEM;

// 如果这个区域跨越了相邻的区域,可能需要分割区域
addr = address;
count = 0;
while(count < size) {
mr = memory_mapping(uc, addr);
len = (size_t)MIN(size - count, mr->end - addr);
if (!split_region(uc, mr, addr, len, true))
return UC_ERR_NOMEM;

// 取消映射
mr = memory_mapping(uc, addr);
if (mr != NULL)
uc->memory_unmap(uc, mr);
count += len;
addr += len;
}

return UC_ERR_OK;
}

使用示例:

if ((err = uc_mem_map(uc, BASE, 0x1000, UC_PROT_ALL))) {
uc_perror("uc_mem_map", err);
return 1;
}

if ((err = uc_mem_unmap(uc, BASE, 0x1000))) {
uc_perror("uc_mem_unmap", err);
return 1;
}

uc_mem_protect

uc_err uc_mem_protect(uc_engine *uc, uint64_t address, size_t size, uint32_t perms);

设置模拟内存的权限

@uc: uc_open() 返回的句柄
@address: 要映射到的新内存区域的起始地址。这个地址必须与4KB对齐,否则将返回UC_ERR_ARG错误。
@size: 要映射到的新内存区域的大小。这个大小必须是4KB的倍数,否则将返回UC_ERR_ARG错误。
@perms: 映射区域的新权限。参数必须是UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC或这些的组合,否则返回UC_ERR_ARG错误。

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_mem_protect(struct uc_struct *uc, uint64_t address, size_t size, uint32_t perms)
{
MemoryRegion *mr;
uint64_t addr = address;
size_t count, len;
bool remove_exec = false;

if (size == 0)
// trivial case, no change
return UC_ERR_OK;

// address must be aligned to uc->target_page_size
if ((address & uc->target_page_align) != 0)
return UC_ERR_ARG;

// size must be multiple of uc->target_page_size
if ((size & uc->target_page_align) != 0)
return UC_ERR_ARG;

// check for only valid permissions
if ((perms & ~UC_PROT_ALL) != 0)
return UC_ERR_ARG;

if (uc->mem_redirect) {
address = uc->mem_redirect(address);
}

// check that user's entire requested block is mapped
if (!check_mem_area(uc, address, size))
return UC_ERR_NOMEM;

// Now we know entire region is mapped, so change permissions
// We may need to split regions if this area spans adjacent regions
addr = address;
count = 0;
while(count < size) {
mr = memory_mapping(uc, addr);
len = (size_t)MIN(size - count, mr->end - addr);
if (!split_region(uc, mr, addr, len, false))
return UC_ERR_NOMEM;

mr = memory_mapping(uc, addr);
// will this remove EXEC permission?
if (((mr->perms & UC_PROT_EXEC) != 0) && ((perms & UC_PROT_EXEC) == 0))
remove_exec = true;
mr->perms = perms;
uc->readonly_mem(mr, (perms & UC_PROT_WRITE) == 0);

count += len;
addr += len;
}

// if EXEC permission is removed, then quit TB and continue at the same place
if (remove_exec) {
uc->quit_request = true;
uc_emu_stop(uc);
}

return UC_ERR_OK;
}

使用示例:

if ((err = uc_mem_protect(uc, BASE, 0x1000, UC_PROT_ALL))) {  //可读可写可执行
uc_perror("uc_mem_protect", err);
return 1;
}

uc_mem_regions

uc_err uc_mem_regions(uc_engine *uc, uc_mem_region **regions, uint32_t *count);

检索由 uc_mem_map() 和 uc_mem_map_ptr() 映射的内存的信息。

这个API为@regions分配内存,用户之后必须通过free()释放这些内存来避免内存泄漏。

@uc: uc_open() 返回的句柄
@regions: 指向 uc_mem_region 结构体的数组的指针. 由Unicorn申请,必须通过uc_free()释放这些内存
@count: 指向@regions中包含的uc_mem_region结构体的数量的指针

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码分析

uint32_t uc_mem_regions(uc_engine *uc, uc_mem_region **regions, uint32_t *count)
{
uint32_t i;
uc_mem_region *r = NULL;

*count = uc->mapped_block_count;

if (*count) {
r = g_malloc0(*count * sizeof(uc_mem_region));
if (r == NULL) {
// 内存不足
return UC_ERR_NOMEM;
}
}

for (i = 0; i < *count; i++) {
r[i].begin = uc->mapped_blocks[i]->addr;
r[i].end = uc->mapped_blocks[i]->end - 1;
r[i].perms = uc->mapped_blocks[i]->perms;
}

*regions = r;

return UC_ERR_OK;
}

使用示例:

#include <iostream>
#include <string>
#include "unicorn/unicorn.h"
using namespace std;

int main()
{
uc_err err;
uc_engine* uc;

if ((err = uc_open(UC_ARCH_X86, UC_MODE_64, &uc))) {
uc_perror("uc_open", err);
return 1;
}

if ((err = uc_mem_map(uc, BASE, 0x1000, UC_PROT_ALL))) {
uc_perror("uc_mem_map", err);
return 1;
}

uc_mem_region *region;
uint32_t count;

if ((err = uc_mem_regions(uc, &region, &count))) {
uc_perror("uc_mem_regions", err);
return 1;
}

cout << "起始地址: 0x" << hex << region->begin << " 结束地址: 0x" << hex << region->end << " 内存权限: " <<region->perms << " 已申请内存块数: " << count << endl;

if ((err = uc_free(region))) { ////注意释放内存
uc_perror("uc_free", err);
return 1;
}

return 0;
}

输出

image.png

uc_free

uc_err uc_free(void *mem);

释放由 uc_context_alloc 和 uc_mem_regions 申请的内存

@mem: 由uc_context_alloc (返回 *context), 或由 uc_mem_regions (返回 *regions)申请的内存

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_free(void *mem)
{
g_free(mem);
return UC_ERR_OK;
}

void g_free(gpointer ptr)
{
free(ptr);
}

使用示例同uc_mem_regions

uc_context_alloc

uc_err uc_context_alloc(uc_engine *uc, uc_context **context);

分配一个可以与uccontext{save,restore}一起使用的区域来执行CPU上下文的快速保存/回滚,包括寄存器和内部元数据。上下文不能在具有不同架构或模式的引擎实例之间共享。

@uc: uc_open() 返回的句柄
@context: 指向uc_engine*的指针。当这个函数成功返回时,将使用指向新上下文的指针更新它。之后必须使用uc_free()释放这些分配的内存。

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_context_alloc(uc_engine *uc, uc_context **context)
{
struct uc_context **_context = context;
size_t size = cpu_context_size(uc->arch, uc->mode);

*_context = malloc(size + sizeof(uc_context));
if (*_context) {
(*_context)->size = size;
return UC_ERR_OK;
} else {
return UC_ERR_NOMEM;
}
}

使用示例

#include <iostream>
#include <string>
#include "unicorn/unicorn.h"
using namespace std;

#define ADDRESS 0x1000
#define X86_CODE32_INC "\x40" // INC eax

int main()
{
uc_engine* uc;
uc_context* context;
uc_err err;

int r_eax = 0x1; // EAX 寄存器

printf("===================================\n");
printf("Save/restore CPU context in opaque blob\n");

err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc);
if (err) {
printf("Failed on uc_open() with error returned: %u\n", err);
return 0;
}

uc_mem_map(uc, ADDRESS, 8 * 1024, UC_PROT_ALL);

if (uc_mem_write(uc, ADDRESS, X86_CODE32_INC, sizeof(X86_CODE32_INC) - 1)) {
printf("Failed to write emulation code to memory, quit!\n");
return 0;
}

// 初始化寄存器
uc_reg_write(uc, UC_X86_REG_EAX, &r_eax);

printf(">>> Running emulation for the first time\n");

err = uc_emu_start(uc, ADDRESS, ADDRESS + sizeof(X86_CODE32_INC) - 1, 0, 0);
if (err) {
printf("Failed on uc_emu_start() with error returned %u: %s\n",
err, uc_strerror(err));
}

printf(">>> Emulation done. Below is the CPU context\n");

uc_reg_read(uc, UC_X86_REG_EAX, &r_eax);
printf(">>> EAX = 0x%x\n", r_eax);

// 申请并保存 CPU 上下文
printf(">>> Saving CPU context\n");

err = uc_context_alloc(uc, &context);
if (err) {
printf("Failed on uc_context_alloc() with error returned: %u\n", err);
return 0;
}

err = uc_context_save(uc, context);
if (err) {
printf("Failed on uc_context_save() with error returned: %u\n", err);
return 0;
}

printf(">>> Running emulation for the second time\n");

err = uc_emu_start(uc, ADDRESS, ADDRESS + sizeof(X86_CODE32_INC) - 1, 0, 0);
if (err) {
printf("Failed on uc_emu_start() with error returned %u: %s\n",
err, uc_strerror(err));
}

printf(">>> Emulation done. Below is the CPU context\n");

uc_reg_read(uc, UC_X86_REG_EAX, &r_eax);
printf(">>> EAX = 0x%x\n", r_eax);

// 恢复 CPU 上下文
err = uc_context_restore(uc, context);
if (err) {
printf("Failed on uc_context_restore() with error returned: %u\n", err);
return 0;
}

printf(">>> CPU context restored. Below is the CPU context\n");

uc_reg_read(uc, UC_X86_REG_EAX, &r_eax);
printf(">>> EAX = 0x%x\n", r_eax);

// 释放 CPU 上下文
err = uc_free(context);
if (err) {
printf("Failed on uc_free() with error returned: %u\n", err);
return 0;
}

uc_close(uc);

return 0;
}

输出

image.png

uc_context_save

uc_err uc_context_save(uc_engine *uc, uc_context *context);

保存当前CPU上下文

@uc: uc_open() 返回的句柄
@context: uc_context_alloc() 返回的句柄

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_context_save(uc_engine *uc, uc_context *context)
{
struct uc_context *_context = context;
memcpy(_context->data, uc->cpu->env_ptr, _context->size);
return UC_ERR_OK;
}

使用示例同uc_context_alloc()

uc_context_restore

uc_err uc_context_restore(uc_engine *uc, uc_context *context);

恢复已保存的CPU上下文

@uc: uc_open() 返回的句柄
@context: uc_context_alloc() 返回并且已使用 uc_context_save 保存的句柄

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型

源码实现

uc_err uc_context_restore(uc_engine *uc, uc_context *context)
{
struct uc_context *_context = context;
memcpy(uc->cpu->env_ptr, _context->data, _context->size);
return UC_ERR_OK;
}

使用示例同uc_context_alloc()

uc_context_size

size_t uc_context_size(uc_engine *uc);

返回存储cpu上下文所需的大小。可以用来分配一个缓冲区来包含cpu上下文,并直接调用uc_context_save。

@uc: uc_open() 返回的句柄

@return 存储cpu上下文所需的大小,类型为 size_t.

源码实现

size_t uc_context_size(uc_engine *uc)
{
return cpu_context_size(uc->arch, uc->mode);
}

static size_t cpu_context_size(uc_arch arch, uc_mode mode)
{
switch (arch) {
#ifdef UNICORN_HAS_M68K
case UC_ARCH_M68K: return M68K_REGS_STORAGE_SIZE;
#endif
#ifdef UNICORN_HAS_X86
case UC_ARCH_X86: return X86_REGS_STORAGE_SIZE;
#endif
#ifdef UNICORN_HAS_ARM
case UC_ARCH_ARM: return mode & UC_MODE_BIG_ENDIAN ? ARM_REGS_STORAGE_SIZE_armeb : ARM_REGS_STORAGE_SIZE_arm;
#endif
#ifdef UNICORN_HAS_ARM64
case UC_ARCH_ARM64: return mode & UC_MODE_BIG_ENDIAN ? ARM64_REGS_STORAGE_SIZE_aarch64eb : ARM64_REGS_STORAGE_SIZE_aarch64;
#endif
#ifdef UNICORN_HAS_MIPS
case UC_ARCH_MIPS:
if (mode & UC_MODE_MIPS64) {
if (mode & UC_MODE_BIG_ENDIAN) {
return MIPS64_REGS_STORAGE_SIZE_mips64;
} else {
return MIPS64_REGS_STORAGE_SIZE_mips64el;
}
} else {
if (mode & UC_MODE_BIG_ENDIAN) {
return MIPS_REGS_STORAGE_SIZE_mips;
} else {
return MIPS_REGS_STORAGE_SIZE_mipsel;
}
}
#endif
#ifdef UNICORN_HAS_SPARC
case UC_ARCH_SPARC: return mode & UC_MODE_SPARC64 ? SPARC64_REGS_STORAGE_SIZE : SPARC_REGS_STORAGE_SIZE;
#endif
default: return 0;
}
}

使用示例同uc_context_alloc()

From https://kabeor.github.io/Unicorn-CPU模拟框架数据类型及API分析与示例(四)//) bye

This blog is under a CC BY-NC-SA 4.0 Unported License
本文链接:https://kabeor.github.io/Unicorn-CPU模拟框架数据类型及API分析与示例(四)/