QEMU专栏 - QEMU 中如何加载ELF文件及Generic Loader初步引入

写在最前

这一章的构造的时间比较久, 最开始我只是想写下ELF文件的读取,但是感觉这样的话,和前面的博客的内容的过渡太生硬了. 同时不知道大家在看到之前的博客中的命令时,会不会好奇这些命令是怎么执行的, 我这边是比较好奇的 ,同时为了更加了解这部分的内容, 所以稍微了解了下.

QEMU Command Option 追踪流程

在之前的博客中, .vscode/tasks.json 中通过配置了QEMU的命令来调试:

<Path>/build/qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -kernel ${workspaceFolder}/build/gcc/output/RTOSDemo.out -monitor none -nographic -serial stdio -s -S

那这些命令,QEMU中是怎么解析处理的呢, 这里以 -kernel以及 -device 进行举例.

QEMU 命令配置

根目录下的qemu-option.hx中定义了 device 以及kernel 的用法.
kernel 选项的用法 :

SRST

The kernel options were designed to work with Linux kernels although other things (like hypervisors) can be packaged up as a kernel executable image. The exact format of a executable image is usually architecture specific.

The way in which the kernel is started (what address it is loaded at, what if any information is passed to it via CPU registers, the state of the hardware when it is started, and so on) is also architecture specific. Typically it follows the specification laid down by the Linux kernel for how kernels for that architecture must be started.

ERST

DEF("kernel", HAS_ARG, QEMU_OPTION_kernel, \
    "-kernel bzImage use 'bzImage' as kernel image\n", QEMU_ARCH_ALL)
SRST
``-kernel bzImage``
    Use bzImage as kernel image. The kernel can be either a Linux kernel
    or in multiboot format.
ERST

device选项的描述比较多, 这里只截取一部分 :


DEF("device", HAS_ARG, QEMU_OPTION_device,
    "-device driver[,prop[=value][,...]]\n"
    "                add device (based on driver)\n"
    "                prop=value,... sets driver properties\n"
    "                use '-device help' to print all possible drivers\n"
    "                use '-device driver,help' to print all possible properties\n",
    QEMU_ARCH_ALL)
SRST
``-device driver[,prop[=value][,...]]``
    Add device driver. prop=value sets driver properties. Valid
    properties depend on the driver. To get help on possible drivers and
    properties, use ``-device help`` and ``-device driver,help``.

    Some drivers are:
....
ERST

其他的选项也是类似, 找到对应的DEF("xxxx"....)即可.

meson.build中可以看到, 在编译时,通过hxtoolqemu-options.hx转换成qemu-options.def:

hxdep = []
hx_headers = [
  ['qemu-options.hx', 'qemu-options.def'],
  ['qemu-img-cmds.hx', 'qemu-img-cmds.h'],
]
if have_system
  hx_headers += [
    ['hmp-commands.hx', 'hmp-commands.h'],
    ['hmp-commands-info.hx', 'hmp-commands-info.h'],
  ]
endif
foreach d : hx_headers
  hxdep += custom_target(d[1],
                input: files(d[0]),
                output: d[1],
                capture: true,
                command: [hxtool, '-h', '@INPUT0@'])
endforeach
genh += hxdep

而转换工具hxtool则是hxtool = find_program('scripts/hxtool'), 继续查看scripts/hxtool中的代码:

#!/bin/sh

hxtoh()
{
    flag=1
    while read -r str; do
        case $str in
            HXCOMM*)
            ;;
            SRST*|ERST*) flag=$(($flag^1))
            ;;
            *)
            test $flag -eq 1 && printf "%s\n" "$str"
            ;;
        esac
    done
}

case "$1" in
"-h") hxtoh ;;
*) exit 1 ;;
esac < "$2"

exit 0

编译后, 可以将qemu-options.hx转换成qemu-options.def, 并且在system/vl.c 中通过include的方式引入.

可以看到在system/vl.c中看到枚举相关的代码:


enum {

#define DEF(option, opt_arg, opt_enum, opt_help, arch_mask)     \
    opt_enum,
#define DEFHEADING(text)
#define ARCHHEADING(text, arch_mask)

#include "qemu-options.def"
};

所以,在后面的代码中,我们只需要关注opt_enum, 在这篇博客中,暂时只关注QEMU_OPTION_kernelQEMU_OPTION_device.

QEMU 命令处理

kernel 选项的处理

在上篇博客中的使用方式为: -kernel ${workspaceFolder}/build/gcc/output/RTOSDemo.out. 从上个章节中, 可以看出, kernel选项的处理是在system/vl.c中的qemu_init方法中,观察switch(popt->index)对应的case ,这个就是上面看到的opt_enumQEMU_OPTION_kernel.

这里先说kernel的配置, 首先找到QEMU_OPTION_kernel的case, 可以看到,其对应的代码非常简单:

case QEMU_OPTION_kernel:
    qdict_put_str(machine_opts_dict, "kernel", optarg);
    break;

即,将kernel对应的参数放置到machine_opts_dict中, 而随后, 通过qemu_create_machine创建对应的machine, 由于Machine实际也是遵守QOM的规则, 所以这里可以找到对应的Machine类型:mps2-an385,TYPE_MPS2_AN385_MACHINE , 它的parentTYPE_MPS2_MACHINE, 再上一层为TYPE_MACHINE, 然后我们在hw/core/machine.cmachine_class_init中可以看到:

object_class_property_add_str(oc, "kernel", machine_get_kernel, machine_set_kernel);
object_class_property_set_description(oc, "kernel", "Linux kernel image file");

这里给对应的class增加了名为kernelStringProperty, 其getset方法分别对machine_get_kernelmachine_set_kernel:`


static char *machine_get_kernel(Object *obj, Error **errp)
{
    MachineState *ms = MACHINE(obj);

    return g_strdup(ms->kernel_filename);
}

static void machine_set_kernel(Object *obj, const char *value, Error **errp)
{
    MachineState *ms = MACHINE(obj);

    g_free(ms->kernel_filename);
    ms->kernel_filename = g_strdup(value);
}

而后,在qemu_apply_machine_options中,通过object_set_properties_from_keyval这个方法,可以循环调用machine_opts_dict对应的class中的属性的set方法, 至此, 我们就已经将命令行中的kernel的参数设置到了MachineState 中的kernel_filename字段中了. 而后, 则是在mps2_common_init中,通过armv7m_load_kernelelf进行读取并处理.

device 选项的处理

在上一篇博客中,我们并没有使用 -device 选项, 在QEMU中有一个Generic Loader的设备可以将镜像或者数据在QEMU启动时加载进去, 如果不清楚Generic Loader的使用, 可以通过-device loader,help查看帮助:

$build/qemu-system-arm -device loader,help     
loader options:
  addr=<uint64>          -  (default: 0)
  cpu-num=<uint32>       -  (default: 4294967295)
  data-be=<bool>         -  (default: false)
  data-len=<uint8>       -  (default: 0)
  data=<uint64>          -  (default: 0)
  file=<str>
  force-raw=<bool>       -  (default: false)

也可以参考docs\system\generic-loader.rst中的Loading Files部分:


Loading Files
^^^^^^^^^^^^^

The loader device also allows files to be loaded into memory. It can load ELF,
U-Boot, and Intel HEX executable formats as well as raw images.  The syntax is
shown below:

    -device loader,file=<file>[,addr=<addr>][,cpu-num=<cpu-num>][,force-raw=<raw>]

``<file>``
  A file to be loaded into memory

``<addr>``
  The memory address where the file should be loaded. This is required
  for raw images and ignored for non-raw files.

``<cpu-num>``
  This specifies the CPU that should be used. This is an
  optional argument and will cause the CPU's PC to be set to the
  memory address where the raw file is loaded or the entry point
  specified in the executable format header. This option should only
  be used for the boot image. This will also cause the image to be
  written to the specified CPU's address space. If not specified, the
  default is CPU 0.

``<force-raw>``
  Setting 'force-raw=on' forces the file to be treated as a raw image.
  This can be used to load supported executable formats as if they
  were raw.

All values are parsed using the standard QemuOpts parsing. This allows the user
to specify any values in any format supported. By default the values
will be parsed as decimal. To use hex values the user should prefix the number
with a '0x'.

An example of loading an ELF file which CPU0 will boot is shown below::

    -device loader,file=./images/boot.elf,cpu-num=0

这里只使用loader来进行加载ELF镜像,使用方式为: -device loader,file=${workspaceFolder}/build/gcc/output/RTOSDemo.out,cpu-num=0. 同理, 在system/vl.c中,可以找到QEMU_OPTION_device对应的case:

case QEMU_OPTION_device:
    if (optarg[0] == '{') {
        QObject *obj = qobject_from_json(optarg, &error_fatal);
        DeviceOption *opt = g_new0(DeviceOption, 1);
        opt->opts = qobject_to(QDict, obj);
        loc_save(&opt->loc);
        assert(opt->opts != NULL);
        QTAILQ_INSERT_TAIL(&device_opts, opt, next);
    } else {
        if (!qemu_opts_parse_noisily(qemu_find_opts("device"),
                                        optarg, true)) {
            exit(1);
        }
    }

这部分代码可以看出, -device后面是支持json格式 .qemu_opts_parse_noisily 这个方法是将optarg解析成QemuOpts的方法, 通过qemu_find_opts("device")找到对应的QemuOpts结构, 然后通过qemu_opts_parse_noisily解析optarg, 简单来说, 就是检测参数是否合理.

后续,在qemu_create_cli_devices中, 通过qemu_opts_foreach遍历device参数,并调用qdev_device_add 方法, 这个方法是将device添加到qom中的方法, 通过qdev_device_add方法,可以将device添加到QOM中, 从而在QEMU启动后, 可以通过QEMU Monitor查看到对应的qom-tree:

(qemu) info qom-tree
/machine (mps2-an385-machine)
.....
  /peripheral (container)
  /peripheral-anon (container)
    /device[0] (loader)

可以看到, loader已经被添加到了QOM中, 并且可以通过info qom查看到对应的qom信息. 根据qom我们快速定位到Generic Loader对应的设备为TYPE_GENERIC_LOADER, 在hw/core/gen-loader.c中, 而当设备初始化的后,就会调用realize方法, 在generic_loader_realize方法中, 同理也会调用加载ELF相关文件的方法, 至此, 就可以使用 -device loader来加载ELF文件了.

需要注意的是,使用Generic Loader加载ELF文件时,不能同时使用 -kernel选项, 否则会报错.

ELF 简诉

为什么要了解ELF 文件格式

在上面, 使用kernel选项及Generic Loader选项加载ELF文件, 那么, 什么是ELF文件呢? ELF 文件中包含哪些信息呢?

什么是ELF

ELF ( Executable and Linkable Format/Extensible Linking Format ) 定义了二进制文件,库和核心文件。 形式化规范允许操作系统正确地解释其底层机器指令。ELF文件通常是编译器或链接器的输出,并且是二进制格式。

ELF 文件结构

首先使用file 查看文件类型:

$ file build/gcc/output/RTOSDemo.out
build/gcc/output/RTOSDemo.out: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

使用readelf 来看查看文件结构, 这里只简单的提下ELF Header, 可以使用readelf -h 读取ELF Header :

$ readelf -h ./build/gcc/output/RTOSDemo.out
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x2719
  Start of program headers:          52 (bytes into file)
  Start of section headers:          345116 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         227
  Section header string table index: 226
  • 前4个字节以固定的头7F 45 4C 46开始 .
  • 第5个字节 就是判断文件的Architecture, 即 32位或者64位 . 其中1表示32位 , 2表示64位.
  • 第6个字节为 数据字段, 用于判断 大小端. 1表示LSB (小端), 2表示MSB(大端).
  • 第7个字节为 固定值1 ,版本, 目前只支持1.

更多的可以参考下面的参考资料.

除了ELF Header之外, ELF文件还包含Program Header TableSection Header Table, Program Header Table包含了ELF文件的segment信息, 而Section Header Table包含了section的信息.

分析ELF工具推荐

  • iaito : https://github.com/radareorg/iaito
    iaitoradare2的图形化界面.

写在最后

这期本来是想分为两篇, 命令加载以及ELF 文件格式介绍, 但是想着ELF文件格式可能需要关注的没有那么多, 所以就只写这部分的内容,这是为了和后续QEMU加载ELF文件的内容做一个过渡. 下期应该会写下QEMU中如何加载ELF文件的内容.

参考资料

  • https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
  • https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/
  • http://www.skyfree.org/linux/references/ELF_Format.pdf
  • https://qemu-project.gitlab.io/qemu/devel/qom.html
  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值