写在最前
这一章的构造的时间比较久, 最开始我只是想写下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
中可以看到, 在编译时,通过hxtool
将qemu-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_kernel
和QEMU_OPTION_device
.
QEMU 命令处理
kernel 选项的处理
在上篇博客中的使用方式为: -kernel ${workspaceFolder}/build/gcc/output/RTOSDemo.out
. 从上个章节中, 可以看出, kernel
选项的处理是在system/vl.c
中的qemu_init
方法中,观察switch(popt->index)
对应的case
,这个就是上面看到的opt_enum
即QEMU_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
, 它的parent
为TYPE_MPS2_MACHINE
, 再上一层为TYPE_MACHINE
, 然后我们在hw/core/machine.c
的machine_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
增加了名为kernel
的StringProperty
, 其get
和set
方法分别对machine_get_kernel
及machine_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_kernel
将elf
进行读取并处理.
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 Table
和Section Header Table
, Program Header Table
包含了ELF
文件的segment
信息, 而Section Header Table
包含了section
的信息.
分析ELF工具推荐
- iaito : https://github.com/radareorg/iaito
iaito
是radare2
的图形化界面.
写在最后
这期本来是想分为两篇, 命令加载以及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