ELF可执行文件的“汇编器”
项目描述
ELFHex
这不是一个官方支持的产品。
ELFHex是一个简单的“汇编器”,专为学习机器代码而设计。它将包含机器指令的程序打包成简单的32位ELF可执行二进制文件。它旨在进行必要的最小转换,以保持输出二进制文件可理解且易于与源文件关联。尽管如此,它还具备一些语言特性(而不仅仅是构建ELF头),使其比仅使用十六进制编辑器编写可执行文件更为方便。
使用方法
ELFHex至少需要Python 3.6。要安装该软件包,请运行
pip install elfhex
这会安装一个名为 elfhex
的命令行工具。使用 elfhex -h
查看其用法。通常,该工具需要一个输入源文件和一个输出可执行文件的存放位置。其他选项包括在输出中省略ELF头以及设置起始内存地址和入口点标签。
开发
本项目使用Python 3.6和pipenv
。为了安装依赖项,运行pipenv install --dev
。然后可以使用python -m elfhex
运行程序。要执行测试,运行pytest
。
有一些预提交钩子可以检查某些格式和其他问题,可以使用pre-commit install
安装。运行black .
来自动格式化更改,以及使用flake8
检查PEP 8违规。
要构建一个包,首先生成requirements.txt
,然后使用setuptools
构建可分发工件。
pipenv run pipenv_to_requirements
pipenv run python setup.py sdist bdist_wheel
作为一个模块
ELFHex模块也可以导入。可以在__main__.py
中看到命令行工具对其的使用。通常,有处理预处理、转换、渲染和ELF头的独立组件。
import elfhex
file_loader = elfhex.FileLoader(include_paths)
preprocessor = elfhex.Preprocessor(file_loader)
preprocessed = preprocessor.preprocess(input_path, max_fragment_depth)
program = elfhex.Transformer().transform(preprocessed)
header = elfhex.elf.get_header(entry_label)
program.prepend_header_to_first_segment(header)
output = program.render(memory_start)
语言参考
源文件是以.eh
格式编写的。每个EH文件包含程序声明、其他源文件的包含、对应于输出ELF文件段的段以及可以复制到段代码中的片段。每个段都有一个名称、各种参数和内容。
program 3 < 4096 # program declaration
include "file.eh" # segments will be merged by name
include fragments "some/file.eh" # only fragments will be included
segment segment_name(flags: rx) {
1e e7 =10d4 "string" # bytes, numbers, and strings
[label] <label:4> <<label + 4>> # labels and references
@fragment_name(aa, <label>) # fragment references
[[auto_label: 4 auto_label2: 1024]] # auto labels
}
fragment fragment_name(arg1, arg2) {
$arg1 # fragment parameter references
ab cd # otherwise, the same content as segments except for auto labels
}
部分(例如,.text
、.bss
等)没有表示,因为在可执行ELF文件中它们不是必要的。在处理完所有包含之后,必须至少有一个定义名为_start
(可配置)的标签的段。此标签将用作程序执行的入口点。
程序声明
每个源文件都必须以程序声明开始。这定义了目标机器(ELF头中e_machine
的数值),文件的字节序,即<
(小端)或>
(大端),以及默认段对齐。例如,program 3 < 4096
将表示e_machine
值为3(x86),小端输出,默认对齐为4096字节。
所有包含的源文件必须具有相同的e_machine
值和字节序,否则会发出错误。如果对齐不同,则使用整体中最大的值。
段
所有段在源代码中都有名称。此名称不会转移到打包的二进制文件中,但在处理包含和引用时作为参考。段还可以定义元数据,包括标志、大小和对齐。使用flags
(r、rw、rx或rwx)、size
(以字节为单位)和alignment
来构建段的程序头条目。size
参数和实际内容的长度中的最大值用于确定程序头条目中提供的内存大小。默认段对齐为0x1000字节,这是x86页面大小。通常不需要更改此值,低于此值可能会导致问题。
片段
片段类似于段,但不定义元数据。它们的内容也类似于段,但它们本身不显示在输出中。相反,可以使用@fragment_name(args)
从段和其他片段中引用片段。这将直接在引用的位置插入片段的内容。片段本身也可以包含对其他片段的引用。在这种情况下,引用将迭代解析,直到达到可配置的限制。如果达到该限制,则发出错误。
片段参数
片段还可以定义参数。这些参数随后可以在片段体中使用 $parameter_name
来引用。这将在引用片段时,在那个点插入参数提供的内 容。当引用片段时,每个参数的值以逗号分隔的列表形式放在片段名称后面的括号中。
任何字节序列都可以用作参数的值,包括标签、引用、其他片段引用,甚至参数值。例如,以下内容是有效的
fragment f1(a, b) {
@f2($b ff $a @f2($a dd))
}
fragment f2(a) {
$a ee
}
在这种情况下,@f1(00, 11)
将产生 11 ff 00 ee 00 dd ee ee
。
名称冲突预防
由于同一个片段可以多次被引用,片段中的标签可能会被定义多次。为了避免这种情况,片段可以定义“局部”标签和引用,这些标签和引用以 __
(例如[__only_use_in_fragment]
) 前缀。这些标签在编译时被覆盖,使得每次对片段的引用都是不同的。这允许同一个片段被多次引用。如果片段包含一个裸标签但需要多次引用,片段的使用者也可以通过使用片段别名语法来防止名称冲突,例如 @fragment_name(args)(alias)
。这将在这个片段实例中的所有标签前加上 alias.
,再次确保它们不会冲突,同时也允许它们在片段外部被引用。
在某些情况下,可能只希望程序中包含一个片段。在这些情况下,从片段引用开始使用 @!
(例如,@!fragment_name()
) 将确保该片段在所有引用中(也以 @!
开头)只包含一次。这不会影响不以 @!
开头的引用。
段和片段内容
一般来说,段或片段的内容是字节序列。这些字节可以表示为字面量,也可以表示为引用。
字面量
最基本的是使用字面量十六进制字节,例如 a1 b8
。
为了方便起见,字节也可以使用二进制、十进制或十六进制的填充字面量来表示,例如 =10d4
(0a 00 00 00
),+2ah2
(2a 00
) 或 -01011011b
(a5
)。在这些情况下,第一个字符代表符号(如果使用 =
,则进行无符号转换),后面的数字代表数字本身,末尾的字符表示基(b
表示二进制,d
表示十进制,h
表示十六进制),基字符后面的数字表示字面量将填充的字节数(默认为单个字节)。
也支持字符串字面量,例如 "Hello"
(48 65 6c 6c 6f
)。这些不会以空字符结尾,而是直接转换为每个字符的ASCII值。只能表示可打印字符;对于其他字符,应直接写出十六进制字节(例如,"test" 0a 00
)。
标签和引用
为了支持指向代码中其他位置的指针,支持标签和引用。标签使用方括号定义,例如 [a_label]
。然后可以使用 <<a_label>>
和 <a_label>
分别对该标签进行绝对和相对引用。也可以在段之间进行绝对引用,并可以提供常数偏移量,例如 <<segment: a_label + 4>>
。相对引用也可以指定它们的字节数,例如 <a_label:4>
(默认宽度为单个字节)。
在这两种情况下,引用在编译时被替换为适当的字节序列。
片段引用
如上所述,可以通过使用 @
符号引用片段来将其内容插入到源代码中,例如,具有名称 fragment_name
的片段将使用 @fragment_name()
引用。
自动标签
在段落的末尾(但不是片段),可以定义一个“自动标签”列表。列表中的所有标签都放置在一对双方括号之间,例如,[[label1: 4 label2: 8]]
。
每个自动标签包含一个名称和一个宽度。这些标签不指向文件图像中的内容,而是指向文件内容之后的段内存位置。宽度定义了标签位置和下一个工件(其他自动标签)在内存布局中的字节数。段的内存大小将相应调整,以容纳所有定义的自动标签。
例如,考虑一个定义为
segment test() { ab cd [[label: 4 label2: 8]] }
此段的大小为2,但内存大小为14(2 + 4 + 8)。如果段的起始偏移量为1000字节,那么label
将指向偏移量1002,而label2
将指向偏移量1006。这些标签可以像通常一样引用。
自动标签只能出现在段末尾,这样它们就不会覆盖段内容。在文件包含过程中合并段时,自动标签列表也会连接在一起。
扩展
由于ELFHex语言本身相当基础,因此可以使用扩展来增强系统的功能。扩展通过以下方式调用
:extension_name { extension_content }
在此点,extension_content
完全由名为extension_name
的扩展来解析和转换为字节,然后将其放置在最终输出中。
扩展使用Python模块定义。如果在开始处使用单个冒号(:
),则导入elfhex.extensions.extension_name
,而如果使用双冒号(::
),则导入extension_name
。例如,以下代码将调用x86.args
扩展
:x86.args { ecx, [ebx + esi * 8 - 4] }
有关扩展结构的更多信息,请参阅扩展开发文档。
注释
可以在代码中包含注释,前面加#
。此之后到行尾的任何字符都将被忽略。
包含
可以使用包含语句将多个源文件组合成一个可执行文件:include "filename.eh"
。这很简单:对于包含文件中的每个段,如果段名称与包含它的文件中的一个匹配,则内容将简单地附加到该段的底部。否则,将创建一个新的段。包含文件中的所有片段也都可用。在包含过程中不更改名称,因此可能发生冲突。应小心命名以减轻这种情况。
如果只需要从文件中导入片段,可以使用include fragments "..."
代替。使用此语句,文件或其包含的段将不会包含到输出中。
递归包含是可能的(即,处理每个包含文件的包含)。如果一个文件已被包含,则忽略其所有后续包含语句。
如前所述,所有包含文件必须在程序声明中具有相同的目标机器和端序。对齐可以不同,但将使用最大值。
示例
此程序简单地打印出“hello, world”五次。
program 3 < 4096
include fragments "other.eh"
segment text(flags: rx) {
[_start]
# print hello to stdout
@syscall3(=4d4, =1d4, <<strings:hello>>, =13d4)
# if ++counter <= 5 goto loop
ff :x86.args{ 0, [dword ptr data:counter] }
81 =00111101b <<data:counter>> =5d4
72 <_start>
@exit()
}
segment data(flags: rw) {
[[counter: 4]]
}
segment strings(flags: r) {
[hello] "Hello, world" 0a
}
other.eh
program 3 < 4096
fragment exit() {
@common_syscall(=1d4)
}
fragment syscall3(number, ebx, ecx, edx) {
bb $ebx
b9 $ecx
ba $edx
@common_syscall($number)
}
fragment syscall(number) {
b8 $number
cd 80
}
有关更多示例,请参阅样本目录。
项目详细信息
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。