充分理解.exe文件是如何执行的
Fully understanding how .exe file is executed
目标
我想了解可执行文件的工作原理。我希望通过全面详细地了解一个非常具体的示例,能够让我做到这一点。我的最终(也许过于雄心勃勃)目标是获取一个 hello-world .exe 文件(使用 C 编译器编译并链接)并详细了解它是如何加载到内存中并由 x86 处理器执行的。如果我成功了,我想写一篇文章and/or制作一个关于它的视频,因为我在互联网上还没有找到这样的东西。
我想问的具体问题已用粗体标出。当然,非常欢迎任何进一步的建议和资源做类似的事情。非常感谢您的帮助!
我需要的
此 Answer 概述了 C 代码在作为程序进入物理内存之前所经历的过程。我还不确定我有多想研究 C 代码的编译方式。 有没有办法查看 C 编译器在汇编之前生成的汇编代码? 我可能认为理解加载和链接过程是值得的。与此同时,我需要了解的最重要的部分是
- PA 可执行文件格式
- 汇编代码与x86的关系byte-code
- 加载过程(即如何使用可执行文件中的信息准备进程 RAM 以供执行)。
我对 PA 格式有一个非常基本的了解(这种理解将在 "What I have learned so far" 部分中概述)并且我认为那里给出的资源应该足够了,我只需要再研究一下直到我足够了解基本的 Hello-World 程序。 当然非常欢迎有关此主题的更多资源。
将byte-code翻译成汇编代码(反汇编)对于x86来说似乎相当困难。尽管如此,我还是很想了解更多。你将如何反汇编一个短字节代码段?
我仍在寻找一种方法来查看进程内存(分配给它的虚拟内存)的内容。我已经研究过 windows-kernel32.dll 函数,例如 ReadProcessMemory
但还不能让它工作。此外,令我感到奇怪的是,似乎没有(免费)工具可用于此。连同对加载的理解,我也许能够理解一个进程如何从 RAM 运行。此外,我正在为汇编程序员寻找允许查看整个进程虚拟内存内容的调试工具。我目前搜索的起点是这个question。关于 我如何查看和理解从 RAM 加载和进程执行,您有进一步的建议吗?
到目前为止我学到了什么
这个 Whosebug 问题的其余部分详细描述了我到目前为止所学的内容,并提供了各种来源。它旨在可重现并帮助任何试图理解这一点的人。但是,到目前为止,我仍然对我看到的例子有一些疑问。
PA 格式
在 Windows 中,可执行文件遵循 PA 格式。 official documentation and this article 很好地概述了格式。该格式描述了 .exe 文件中各个字节的含义。开头是一个 DOS 程序(包括遗留原因),我不会担心。然后是一堆 headers,它们提供有关可执行文件的信息。实际文件内容被分成具有名称的部分,例如“.rdata”。在文件 headers 之后,还有部分 headers,它告诉你文件的哪些部分是哪个部分以及每个部分的作用(例如,如果它包含可执行代码)。
可以使用 dumpbin(用于查看二进制文件的 Microsoft 工具)等工具解析 header 和部分。为了与 dumpbin 输出进行比较,可以直接使用十六进制编辑器甚至使用 Powershell(命令 Format-Hex -Path <Path to file>
)查看文件的十六进制代码。
具体例子
我为一个非常简单的程序执行了这些步骤,它什么也不做。这是代码:
; NASM assembler programm. Does nothing. Stores string in code section.
; Adapted from whosebug.com/a/1029093/9988487
global _main
section .text
_main:
hlt
db 'Hello, World'
我是用NASM(命令nasm -fwin32 filename.asm
)组装的,用VS2019自带的链接器(link /subsystem:console /nodefaultlib /entry:main test.obj
)链接的。这是改编自 this answer,它演示了如何使用 WinAPI 调用为 Windows 制作 hello-world 程序。程序 运行s 在 Windows 10 上结束,没有输出。从 运行 到 大约需要 2 秒,这看起来很长,让我觉得 可能有一些错误 ?
然后我查看了 dumpbin 输出:
D:\ASM>dumpbin test.exe /ALL
Microsoft (R) COFF/PE Dumper Version 14.22.27905.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file test.exe
PE signature found
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (x86)
2 number of sections
5E96C000 time date stamp Wed Apr 15 10:04:16 2020
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
102 characteristics
Executable
32 bit word machine
OPTIONAL HEADER VALUES
10B magic # (PE32)
14.22 linker version
200 size of code
200 size of initialized data
0 size of uninitialized data
1000 entry point (00401000)
1000 base of code
2000 base of data
400000 image base (00400000 to 00402FFF)
1000 section alignment
200 file alignment
<further header values omitted ...>
SECTION HEADER #1
.text name
E virtual size
1000 virtual address (00401000 to 0040100D)
200 size of raw data
200 file pointer to raw data (00000200 to 000003FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read
RAW DATA #1
00401000: F4 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 0A ôHello, World.
SECTION HEADER #2
.rdata name
58 virtual size
2000 virtual address (00402000 to 00402057)
200 size of raw data
400 file pointer to raw data (00000400 to 000005FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only
RAW DATA #2
00402000: 00 00 00 00 00 C0 96 5E 00 00 00 00 0D 00 00 00 .....À.^........
00402010: 3C 00 00 00 1C 20 00 00 1C 04 00 00 00 00 00 00 <.... ..........
00402020: 00 10 00 00 0E 00 00 00 2E 74 65 78 74 00 00 00 .........text...
00402030: 00 20 00 00 1C 00 00 00 2E 72 64 61 74 61 00 00 . .......rdata..
00402040: 1C 20 00 00 3C 00 00 00 2E 72 64 61 74 61 24 7A . ..<....rdata$z
00402050: 7A 7A 64 62 67 00 00 00 zzdbg...
Debug Directories
Time Type Size RVA Pointer
-------- ------- -------- -------- --------
5E96C000 coffgrp 3C 0000201C 41C
Summary
1000 .rdata
1000 .text
文件header字段"characteristics"是标志的组合。特别是 102h = 1 0000 0010b
和两个设置标志(根据 PE 格式文档)是 IMAGE_FILE_EXECUTABLE_IMAGE
和 IMAGE_FILE_BYTES_REVERSED_HI
。后者有描述
IMAGE_FILE_BYTES_REVERSED_HI:
Big endian: the MSB precedes the LSB in memory. This flag is deprecated and should be zero.
我问自己:为什么现代汇编器和现代链接器会产生弃用标志?
文件中有 2 个部分。 .text
部分在 ass 中定义bler 代码(并且是唯一包含可执行代码的代码,如其 header 中所指定)。 我不知道第二部分'.rdata'(名称似乎指的是"readable data")是什么或在这里做什么。为什么要创建它?我怎么知道?
反汇编
我用dumpbin反汇编.exe文件(命令dumpbin test.exe /DISASM
)。它获得 hlt
正确,'Hello, World.' 字符串(可能不幸)被解释为可执行命令。我猜这很难归咎于反汇编程序。但是,如果我理解正确(我在汇编编程方面没有实际经验),将数据放入代码段并不是闻所未闻(在研究汇编编程时发现的几个示例中就是这样做的)。 有没有更好的反汇编方法,可以更好地重现我的汇编代码?另外,编译器有时会以这种方式将数据放入代码段吗?
我认为我无法回答所有问题。我也是初学者,所以我可能会说一些不准确的事情。但是,我会尽力而为,我想我可以给你带来一些东西。
不,编译器不会将数据放在代码段中(如果我错了请纠正我)。有 .data 部分(用于初始化数据)和 .bss 部分(用于未初始化数据)。
我想,我最好向您展示一个打印 hello world 的程序示例(对于 linux,因为它更简单,而且我不知道如何使用 windows。在 x64 中,但它就像 x86。只是系统调用的名称和不同的寄存器。x64 用于 64 位,x86 用于 32 位)。
BITS 64 ;not obligatory but I prefer
section .data
msg db "hello world" ;the message
len equ $-msg ;the length of msg
section .text
global _start
_start: ;the entry point
mov rax, 1 ;syscall 1 to print something
mov rdi, 1 ;1 for stdout
mov rsi, msg ;the message
mov rdx, len ;length in rdx
syscall
mov rax, 60 ;exit syscall
mov rdi, 0 ;exit with 0
syscall
(https://tio.run/#assembly-nasm 如果你不想使用 VM。我建议你寻找 WSL + vscode 如果你正在使用 windows。你将有 [= windows 中的 38=] 和 vscode 中的扩展名可以访问 windows 中的文件,但是
如果你想反汇编代码或查看内存是多少,你可以在linux中使用gdb或radare2。对于windows,还有ghidra、IDA、olly dbg等其他工具..
我不知道有什么方法可以让编译器创建更好的汇编代码。但不代表不存在
我从来没有为 windows 做过任何东西。但是,对于 link 我的目标文件,我使用 ld (我不知道它是否有帮助)。
ld object.o -o compiledprogram
我现在没有时间继续写作,所以我现在不能给你任何课程建议。我稍后再看。
希望对您有所帮助。
在某些方面,这是一个非常广泛的问题,可能因此而无法生存。资料全在网上,自己找找看,不复杂,不值得写论文视频。
所以你有一个粗略的想法,即编译器将用一种语言编写的程序转换为另一种语言,可以是汇编语言或机器代码或其他语言。
然后是文件格式,有许多不同的文件格式,我们都使用术语“二进制”来表示不同的格式。理想情况下,它们使用某种形式的编码包含机器代码和数据或有关数据的信息。
暂时打算用ARM,定长指令方便反汇编阅读等等
#define ONE 1
unsigned int x;
unsigned int y = 5;
const unsigned int z = 7;
unsigned int fun ( unsigned int a )
{
return(a+ONE);
}
和gnu gcc/binutils 因为它非常有名,被广泛使用,你可以用它在你的wintel机器上制作程序。我 运行 Linux 所以你会看到 elf 而不是 exe,但这只是你所要求的文件格式。
arm-none-eabi-gcc -O2 -c so.c -save-temps -o so.o
此工具链(linked 的工具链,例如编译器 -> 汇编器 -> linker)是 Unix 风格和模块化的。您将有一个针对目标的汇编器,所以不确定为什么要 re-invent 那个,通过查看汇编输出来调试编译器比尝试直接进入机器代码要容易得多。但是有些人喜欢爬山只是因为它在那里而不是四处走动,一些工具直接用于机器代码只是因为它在那里。
这个特定的编译器有这个保存临时文件的功能,gcc 本身是一个前端程序,它为真正的编译器做准备然后如果被要求(如果你不说不)将调用汇编器并且 link呃
cat so.i
# 1 "so.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "so.c"
unsigned int x;
unsigned int y = 5;
const unsigned int z = 7;
unsigned int fun ( unsigned int a )
{
return(a+1);
}
所以在这一点上定义和包含被处理并且它的一个大文件被发送到编译器。
编译器完成它的工作并将其转换为汇编语言
cat so.s
.cpu arm7tdmi
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 2
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "so.c"
.text
.align 2
.global fun
.arch armv4t
.syntax unified
.arm
.fpu softvfp
.type fun, %function
fun:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
add r0, r0, #1
bx lr
.size fun, .-fun
.global z
.global y
.comm x,4,4
.section .rodata
.align 2
.type z, %object
.size z, 4
z:
.word 7
.data
.align 2
.type y, %object
.size y, 4
y:
.word 5
.ident "GCC: (GNU) 9.3.0"
然后将其放入目标文件中,在本例中为 binutils,linux 默认等
file so.o
so.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped
它使用的是 elf 文件格式,易于查找信息,易于编写程序进行解析等。
我可以反汇编这个,请注意,因为我正在使用反汇编程序,所以它会尝试反汇编所有内容,即使它不是机器代码,坚持使用 32 位 arm 的东西它可以通过它来研究,当有真正的指令时,他们显示(对齐而不是这里使用的可变长度,所以你可以线性反汇编,你不能使用可变长度指令集并有成功的希望(如x86)你需要按执行顺序反汇编然后你经常会错过一些由于根据程序的性质)
arm-none-eabi-objdump -D so.o
so.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <fun>:
0: e2800001 add r0, r0, #1
4: e12fff1e bx lr
Disassembly of section .data:
00000000 <y>:
0: 00000005 andeq r0, r0, r5
Disassembly of section .rodata:
00000000 <z>:
0: 00000007 andeq r0, r0, r7
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 movtmi r4, #14080 ; 0x3700
4: 4728203a ; <UNDEFINED> instruction: 0x4728203a
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e332e39 mrccs 14, 1, r2, cr3, cr9, {1}
10: Address 0x0000000000000010 is out of bounds.
Disassembly of section .ARM.attributes:
00000000 <.ARM.attributes>:
0: 00002941 andeq r2, r0, r1, asr #18
4: 61656100 cmnvs r5, r0, lsl #2
8: 01006962 tsteq r0, r2, ror #18
c: 0000001f andeq r0, r0, pc, lsl r0
10: 00543405 subseq r3, r4, r5, lsl #8
14: 01080206 tsteq r8, r6, lsl #4
18: 04120109 ldreq r0, [r2], #-265 ; 0xfffffef7
1c: 01150114 tsteq r5, r4, lsl r1
20: 01180317 tsteq r8, r7, lsl r3
24: 011a0119 tsteq r10, r9, lsl r1
28: Address 0x0000000000000028 is out of bounds.
是的,该工具在其中放置了额外的东西,但请注意,主要是我创建的。一些代码,一些初始化的 read/write 数据,一些初始化的 read/write 数据和一些初始化的只读数据。工具链作者可以使用他们想要的任何名称,他们甚至不必使用术语部分。但从几十年的历史、通信和术语来看,.text 通常用于代码(如只读机器代码和相关数据),.bss 用于归零 read/write 数据,尽管我看到过其他名称,.data 用于初始化 read/write 数据和此工具的这一代 .rodata 用于只读初始化数据(技术上可以登陆 .text)
请注意,它们的地址均为零。他们还没有 linked。
现在这很丑陋,但为了避免添加更多代码,如果该工具允许我这样做,让我们 link 制作一个完全无法使用的二进制文件(没有 bootstrap,等等) :
arm-none-eabi-ld -Ttext=0x1000 -Tdata=0x2000 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
arm-none-eabi-objdump -D so.elf
so.elf: file format elf32-littlearm
Disassembly of section .text:
00001000 <fun>:
1000: e2800001 add r0, r0, #1
1004: e12fff1e bx lr
Disassembly of section .data:
00002000 <y>:
2000: 00000005 andeq r0, r0, r5
Disassembly of section .rodata:
00001008 <z>:
1008: 00000007 andeq r0, r0, r7
Disassembly of section .bss:
00002004 <x>:
2004: 00000000 andeq r0, r0, r0
现在是 linked。只读项 .text 和 .rodata 以在文件中找到的顺序落在 .text 地址 space 中。 read/write 项按照在文件中找到的顺序落入 .data 地址 space。
是的,对象中的 .bss 在哪里?它就在那里,它没有作为对象一部分的以字节为单位的实际数据,而是具有名称和大小,并且它是 .bss。无论出于何种原因,该工具确实会从 linked 二进制文件中显示它。
回到术语二进制。 so.elf 二进制文件包含内存中组成程序的字节,还有文件格式基础结构加上符号 table 使反汇编和调试更容易以及其他内容。 Elf 是一种灵活的文件格式,gnu 可以使用它,您会得到一个结果,其他工具或工具版本可以使用它并拥有不同的文件。显然,两个编译器可以从同一个源程序生成不同的机器代码,这不仅仅是由于优化,工作是用目标语言制作一个功能程序,功能是 compiler/tool 作者的意见。
内存映像类型文件呢:
arm-none-eabi-objcopy so.elf so.bin -O binary
hexdump -C so.bin
00000000 01 00 80 e2 1e ff 2f e1 07 00 00 00 00 00 00 00 |....../.........|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001000 05 00 00 00 |....|
00001004
现在 objcopy 工具的工作原理是,它从第一个定义的可加载项或任何您想使用的字节开始,以最后一个字节结束,并使用(零)填充来制作文件大小匹配,以便内存映像从地址角度匹配。星号基本上意味着 0 填充。因为我们从 .text 的 0x1000 开始,.data 从 0x2000 开始,但是这个文件的第一个字节(偏移量 0)是 .text 的开始,后面的 0x1000 字节是文件中的偏移量 0x1000 但我们知道它在内存中转到 0x2000是 read/write 的东西。还要注意 bss zeros 不在输出中。 bootstrap 预计为零 those.
没有诸如此文件中的数据在内存中的位置等信息。如果你想一想如果我在我定义的部分有一个字节去 0x00000000 和一个字节在一个部分怎么办我定义到 0x80000000 并输出这个文件,是的,这是一个 0x80000001 字节的文件,尽管相关信息只有两个有用的字节。一个 2GB 的文件,可以容纳两个字节。这就是为什么在整理好 linker 脚本和工具之前不想输出这种文件格式的原因。
相同的数据和另外两种同样老派的格式,有一点英特尔与摩托罗拉的历史
arm-none-eabi-objcopy so.elf so.hex -O ihex
cat so.hex
:08100000010080E21EFF2FE158
:0410080007000000DD
:0420000005000000D7
:0400000300001000E9
:00000001FF
arm-none-eabi-objcopy so.elf so.srec -O srec
cat so.srec
S00A0000736F2E7372656338
S10B1000010080E21EFF2FE154
S107100807000000D9
S107200005000000D3
S9031000EC
现在这些包含相关字节,加上地址,但没有太多其他信息,每个数据字节需要两个字节以上,但与带有填充的巨大文件相比,值得 trade-off。这两种格式今天都在使用,虽然不如过去那么多,但仍然存在。
无数其他二进制文件格式和像 objdump 这样的工具有一个不错的格式列表,它可以生成以及其他 linkers and/or 工具。
与所有这些相关的是存在某种形式的二进制文件格式,其中包含我们 运行 程序所需的字节。
您可能会问什么格式和什么地址...那是操作系统或系统设计的一部分。在 Windows 的情况下,windows 操作系统(您使用的特定版本)可能支持特定的文件格式和 ose 格式的变体。 Windows 已经确定了地址 space 的样子。像这样的操作系统利用 MMU 来虚拟化地址和保护。拥有一个虚拟地址 space 意味着每个程序都可以住在同一个 space 中。例如,所有程序都可以有一个从零开始的地址....
test.c
int main ( void )
{
return 1;
}
hello.c
int main ( void )
{
return 2;
}
gcc test.c -o test
objdump -D test
Disassembly of section .text:
00000000004003e0 <_start>:
4003e0: 31 ed xor %ebp,%ebp
4003e2: 49 89 d1 mov %rdx,%r9
4003e5: 5e pop %rsi
...
gcc hello.c -o hello
objdump -D hello
Disassembly of section .text:
00000000004003e0 <_start>:
4003e0: 31 ed xor %ebp,%ebp
4003e2: 49 89 d1 mov %rdx,%r9
同一个地址,possible 怎么会不会叠在一起?没有虚拟机。请注意,这是为特定日期的特定 Linux 构建的,等等。工具链有一个默认的 linker 脚本(注意我没有指定如何 link)这个平台当编译器为此构建时 target/platform.
arm-none-eabi-gcc -O2 test.c -c -o test.o
arm-none-eabi-ld test.o -o test.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000008000
arm-none-eabi-objdump -D test.elf
test.elf: file format elf32-littlearm
Disassembly of section .text:
00008000 <main>:
8000: e3a00001 mov r0, #1
8004: e12fff1e bx lr
相同的源代码,相同的编译器,为不同的目标和系统不同的地址构建。
所以对于 Windows 肯定会有支持的二进制格式的规则和可以使用的地址 spaces 的规则,如何定义 ose spaces 在文件中。
然后操作系统启动器读取二进制文件并将可加载项目放入内存中的ose地址(在虚拟space中os 已经为这个特定程序创建了)非常 pos 加载程序的一个功能是为您将 bss 归零,因为信息在那里。低级程序员需要知道 pos 巧妙地处理 .bss 与否。
如果没有,您将看到并且可能需要创建一个解决方案,不幸的是,这是您深入了解工具特定项目的地方。虽然 C 可能在某种程度上是标准化的,但有一些工具特定的东西没有被 tool/authors 标准化,或者至少被 tool/authors 标准化,但没有理由假设 those cross 转移到其他工具。
.globl _start
_start:
ldr sp,sp_init
bl fun
b .
.word __bss_start__
.word __bss_end__
sp_init:
.word 0x8000
关于汇编语言的一切都是特定于工具的,出于理智的原因,助记符无疑会类似于 ip/processor 供应商文档,这些文档使用他们付费开发的工具所使用的语法。但除此之外,汇编语言完全由工具而不是目标定义,x86 因为它的年龄和其他事情真的很糟糕,这不是英特尔与 AT&T 的事情,只是一般而言。 Gnu 汇编程序是众所周知的,因为我认为可能有意不使语言与其他汇编语言兼容。以上是arm的gnu汇编器。
使用上面的 fun() 函数,C 说它应该是 main() 但工具不在乎我已经在这里输入了足够的内容。
添加一个简单的基于 ram 的 linker 脚本
MEMORY
{
ram : ORIGIN = 0x1000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > ram
.rodata : { *(.rodata*) } > ram
.bss : {
__bss_start__ = .;
*(.bss*)
} > ram
__bss_end__ = .;
}
打造一切
arm-none-eabi-as start.s -o start.o
arm-none-eabi-gcc -O2 -c so.c -o so.o
arm-none-eabi-ld -T sram.ld start.o so.o -o so.elf
检查
arm-none-eabi-nm so.elf
0000102c B __bss_end__
00001028 B __bss_start__
00001018 T fun
00001014 t sp_init
00001000 T _start
00001028 B x
00001024 D y
00001020 R z
arm-none-eabi-objdump -D so.elf
so.elf: file format elf32-littlearm
Disassembly of section .text:
00001000 <_start>:
1000: e59fd00c ldr sp, [pc, #12] ; 1014 <sp_init>
1004: eb000003 bl 1018 <fun>
1008: eafffffe b 1008 <_start+0x8>
100c: 00001028 andeq r1, r0, r8, lsr #32
1010: 0000102c andeq r1, r0, r12, lsr #32
00001014 <sp_init>:
1014: 00008000 andeq r8, r0, r0
00001018 <fun>:
1018: e2800001 add r0, r0, #1
101c: e12fff1e bx lr
Disassembly of section .rodata:
00001020 <z>:
1020: 00000007 andeq r0, r0, r7
Disassembly of section .data:
00001024 <y>:
1024: 00000005 andeq r0, r0, r5
Disassembly of section .bss:
00001028 <x>:
1028: 00000000 andeq r0, r0, r0
所以现在 possible 添加到 bootstrap 内存归零循环(不要使用 C/memset 你不会产生先有鸡还是先有蛋的问题 你写bootstrap in asm) 基于开始和结束地址。
幸运或不幸,因为 linker 脚本是工具特定的和汇编语言它是特定于工具的,如果您让这些工具为您完成工作,它们需要一起工作(理智的方法,找出 .bss 在哪里,否则会很有趣)。
这可以在操作系统上完成,但是当你进入微控制器时,所有这些都必须在 non-volatile 存储(闪存)上,那么 pos 可能有一个从其他地方(有时像你的鼠标固件,有时是键盘等)下载到 ram,假设是闪存,那么你如何处理 .data?
MEMORY
{
rom : ORIGIN = 0x0000, LENGTH = 0x1000
ram : ORIGIN = 0x1000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > rom
.rodata : { *(.rodata*) } > rom
.data : {
*(.data*)
} > ram AT > rom
.bss : {
__bss_start__ = .;
*(.bss*)
} > ram
__bss_end__ = .;
}
对于 gnu ld,这基本上表示 .data 的主目录在 ram 中,但输出二进制格式会将其放入 flash/rom
so.elf so.srec -O srec
cat so.srec
S00A0000736F2E7372656338
S11300000CD09FE5030000EBFEFFFFEA04100000A4
S11300100810000000800000010080E21EFF2FE1B4
S107002007000000D1 <- z variable at address 0020
S107002405000000CF <- y variable at 0024
S9030000FC
并且您必须更多地使用 linker 脚本来获得告诉您 ram 和闪存起始地址和结束地址或长度的工具。然后在 bootstrap(asm 不是 C)中添加代码以将 .data 从闪存复制到 ram。
另请注意您的众多问题中的另一个问题。
.word __bss_start__
.word __bss_end__
sp_init:
.word 0x8000
这些项目是技术数据。但它们首先存在于 .text 中,然后存在于 foremost 中,因为它们是在假定为 .text 的代码中定义的(我不需要在 asm 中说明这一点,但可以有)。你也会在 x86 中看到这个,但是对于像 arm、mips、risc-v 等固定长度,你不能在指令本身中放置任何你想要的旧 immediate/constant/linked 值,你将它放在附近的“池”并进行 pc 相关读取以获取它。对于 linking 外部设备,您也会看到这一点:
extern unsigned int x;
int main ( void )
{
return x;
}
arm-none-eabi-gcc -O2 -c test.c -o test.o
arm-none-eabi-objdump -D test.o
test.o: file format elf32-littlearm
Disassembly of section .text.startup:
00000000 <main>:
0: e59f3004 ldr r3, [pc, #4] ; c <main+0xc>
4: e5930000 ldr r0, [r3]
8: e12fff1e bx lr
c: 00000000 andeq r0, r0, r0 <--- the code gets the address of the
从这里变量然后从内存中读取它
一次 linked
Disassembly of section .text:
00008000 <main>:
8000: e59f3004 ldr r3, [pc, #4] ; 800c <main+0xc>
8004: e5930000 ldr r0, [r3]
8008: e12fff1e bx lr
800c: 00018010 andeq r8, r1, r0, lsl r0
Disassembly of section .data:
00018010 <x>:
18010: 00000005 andeq r0, r0, r5
对于 x86
gcc -c -O2 test.c -o test.o
dwelch-desktop so # objdump -D test.o
test.o: file format elf64-x86-64
Disassembly of section .text.startup:
0000000000000000 <main>:
0: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 6 <main+0x6>
6: c3 retq
00000000004003e0 <main>:
4003e0: 8b 05 4a 0c 20 00 mov 0x200c4a(%rip),%eax # 601030 <x>
4003e6: c3 retq
如果你眯着眼睛真的不一样吗?处理器读取附近的数据以加载到寄存器和/或使用。无论哪种方式,由于指令集的性质,linker 修改指令或附近的池数据或两者。
最后一个:
arm-none-eabi-gcc -S test.c
cat test.s
.cpu arm7tdmi
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 6
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "test.c"
.text
.align 2
.global main
.arch armv4t
.syntax unified
.arm
.fpu softvfp
.type main, %function
main:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
str fp, [sp, #-4]!
add fp, sp, #0
ldr r3, .L3
ldr r3, [r3]
mov r0, r3
add sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx lr
.L4:
.align 2
.L3:
.word x
.size main, .-main
.ident "GCC: (GNU) 9.3.0"
所以你能看到汇编语言吗,是的,有些工具会让你保存中间文件and/or让你在编译时生成文件的汇编输出。
你能在代码中有数据吗,是的,有时和有理由在 .text 区域中有数据值,而不仅仅是目标特定你会出于各种原因看到这个,一些工具链把只读数据放在那里。
有许多文件格式,现代操作系统使用的文件格式不仅具有定义构成机器代码和数据值的字节的功能,而且还包括符号和其他调试信息。
程序的文件格式和内存 space 是操作系统特定的,而不是语言甚至目标特定的(Linux、Windows、同一台笔记本电脑上的 MacOS 预计不会尽管目标计算机完全相同,但规则相同)。该平台的本机工具链具有默认的 linker 脚本以及为该目标构建 usable/working 程序所需的任何其他信息。包括支持的文件格式。
机器代码和数据项可以以不同的方式以不同的文件格式表示,目标系统的操作系统或加载程序是否可以使用该格式取决于目标系统。
程序有错误和细微差别。文件格式有版本和不一致,你可能会发现一些 elf 文件格式 reader 只是发现它不起作用或在提供一个在某些系统上工作的非常好的 elf 文件时打印出奇怪的东西。为什么要设置一些标志?也许 those 字节得到了 re-used 或标志重新利用 osed 或数据结构改变或工具以不同的方式或以 non-standard 方式使用它(想想 mov 20h,ax) 和另一个不兼容的工具无法理解或幸运地获得足够的 close。
在 Stack Overflow 上问“为什么”问题不是很有用,找到写这东西的人的几率非常低,询问你从哪里获得工具并跟随希望这个人是的人的几率更高还活着,愿意被打扰。并且 99.999(很多 9)% 没有一套全球性的神圣规则来写这件事 under/for。总的来说,有些家伙只是觉得这就是为什么他们做了他们所做的,没有真正的原因,懒惰,一个错误,故意试图破坏别人的工具。一直到有意见的大型委员会在特定的一天在特定的房间对其进行投票,这就是原因(我们知道当我们由委员会设计或尝试编写没有人遵守的规范时我们会得到什么)。
我知道你在 Windows,我手头没有 Windows 机器,我在 Linux。但是 gnu/binutils 和 clang/llvm 工具很容易获得,并且有一套丰富的工具,如 readelf、nm、objdump 等。这些工具有助于检查事物,一个好的工具至少在内部会有这样的工具对于开发人员来说,他们可以将工具的输出调试到一定的质量水平。 gnu 人制作了工具并让每个人都可以使用它们,虽然这需要时间通过它们和它们的特性,它们对于你想要理解的东西非常强大。
你不会找到一个好的 x86 反汇编器,它们都是垃圾,仅仅是因为野兽的本性。它是一个可变长度的指令集,所以根据定义,除非你正在执行,否则你不能正确地整理它。您必须按执行顺序从一个已知的良好入口点进行反汇编,才有一半的机会,然后由于各种原因,您无法以这种方式看到代码路径(例如,想想跳转 tables,或 dll 或 so 文件) .最好的解决方案是拥有非常 accurate/perfect emulator/simulator 和 运行 的代码,并执行您需要做的所有 actions/gyrations 以使其覆盖所有代码路径,并且让该工具记录来自数据的指令以及每个指令所在的位置或每个没有分支的线性部分。
这样做的好处是,今天很多代码都是使用没有试图隐藏任何东西的工具编译的。在过去,由于各种原因,您会看到手写的 asm,故意试图防止反汇编或由于其他因素(在贸易展前一天为视频游戏手工编辑二进制 rom 图像,去拆解一些经典的 rom) .
mov r0,#0
cmp r0,#0
jz somewhere
.word 0x12345678
反汇编程序无法解决这个问题,有些人可能会为此添加案例
mov r0,#0
nop
nop
xor r0,#1
nop
nop
xor r0,#3
xor r0,#2
cmp r0,#0
jz somewhere
.word 0x12345678
并且它认为数据是一条指令,因为可变长度对于反汇编程序来说很难解决一个像样的指令,至少会检测到指令的非操作码部分分支到 and/or 的冲突指令的操作码部分稍后显示为其他指令中的附加字节。该工具无法解决人类必须解决的问题。
即使使用 arm 和 mips 并具有 32 位和 16 位指令,risc-v 具有可变大小的指令等...
gnu 的反汇编程序经常会被 x86 绊倒。
文中问题的答案:
1、可以看到进程一步步执行,用debugger可以看到进程内存。我使用 OllyDbg 来学习汇编,它是免费且功能强大的调试器。
2. 进程在调用 NtCreateUserProcess 后由 Windows 内核加载,所以我认为您需要内核调试才能了解它是如何完成的。
3. OllyDbg中调试的代码自动反汇编。
4.您可以将只读数据放在“.text”部分。您可以更改节标志使其可写,然后可以混合代码和数据。一些编译器可能会合并“.text”和“.rdata”部分。
我建议您按顺序阅读 PE 导入、导出、迁移和资源。如果您想查看最简单的 i386 PE helloworld,您可以在此处查看我的 hello_world_pe_i386_dynamic.exe 程序:https://github.com/pajacol/hello-world。我完全是用二进制文件编辑器写的。它只包含必需的数据结构。此可执行文件与位置无关,可以在任何地址加载而无需重定位。
目标
我想了解可执行文件的工作原理。我希望通过全面详细地了解一个非常具体的示例,能够让我做到这一点。我的最终(也许过于雄心勃勃)目标是获取一个 hello-world .exe 文件(使用 C 编译器编译并链接)并详细了解它是如何加载到内存中并由 x86 处理器执行的。如果我成功了,我想写一篇文章and/or制作一个关于它的视频,因为我在互联网上还没有找到这样的东西。
我想问的具体问题已用粗体标出。当然,非常欢迎任何进一步的建议和资源做类似的事情。非常感谢您的帮助!
我需要的
此 Answer 概述了 C 代码在作为程序进入物理内存之前所经历的过程。我还不确定我有多想研究 C 代码的编译方式。 有没有办法查看 C 编译器在汇编之前生成的汇编代码? 我可能认为理解加载和链接过程是值得的。与此同时,我需要了解的最重要的部分是
- PA 可执行文件格式
- 汇编代码与x86的关系byte-code
- 加载过程(即如何使用可执行文件中的信息准备进程 RAM 以供执行)。
我对 PA 格式有一个非常基本的了解(这种理解将在 "What I have learned so far" 部分中概述)并且我认为那里给出的资源应该足够了,我只需要再研究一下直到我足够了解基本的 Hello-World 程序。 当然非常欢迎有关此主题的更多资源。
将byte-code翻译成汇编代码(反汇编)对于x86来说似乎相当困难。尽管如此,我还是很想了解更多。你将如何反汇编一个短字节代码段?
我仍在寻找一种方法来查看进程内存(分配给它的虚拟内存)的内容。我已经研究过 windows-kernel32.dll 函数,例如 ReadProcessMemory
但还不能让它工作。此外,令我感到奇怪的是,似乎没有(免费)工具可用于此。连同对加载的理解,我也许能够理解一个进程如何从 RAM 运行。此外,我正在为汇编程序员寻找允许查看整个进程虚拟内存内容的调试工具。我目前搜索的起点是这个question。关于 我如何查看和理解从 RAM 加载和进程执行,您有进一步的建议吗?
到目前为止我学到了什么
这个 Whosebug 问题的其余部分详细描述了我到目前为止所学的内容,并提供了各种来源。它旨在可重现并帮助任何试图理解这一点的人。但是,到目前为止,我仍然对我看到的例子有一些疑问。
PA 格式
在 Windows 中,可执行文件遵循 PA 格式。 official documentation and this article 很好地概述了格式。该格式描述了 .exe 文件中各个字节的含义。开头是一个 DOS 程序(包括遗留原因),我不会担心。然后是一堆 headers,它们提供有关可执行文件的信息。实际文件内容被分成具有名称的部分,例如“.rdata”。在文件 headers 之后,还有部分 headers,它告诉你文件的哪些部分是哪个部分以及每个部分的作用(例如,如果它包含可执行代码)。
可以使用 dumpbin(用于查看二进制文件的 Microsoft 工具)等工具解析 header 和部分。为了与 dumpbin 输出进行比较,可以直接使用十六进制编辑器甚至使用 Powershell(命令 Format-Hex -Path <Path to file>
)查看文件的十六进制代码。
具体例子
我为一个非常简单的程序执行了这些步骤,它什么也不做。这是代码:
; NASM assembler programm. Does nothing. Stores string in code section.
; Adapted from whosebug.com/a/1029093/9988487
global _main
section .text
_main:
hlt
db 'Hello, World'
我是用NASM(命令nasm -fwin32 filename.asm
)组装的,用VS2019自带的链接器(link /subsystem:console /nodefaultlib /entry:main test.obj
)链接的。这是改编自 this answer,它演示了如何使用 WinAPI 调用为 Windows 制作 hello-world 程序。程序 运行s 在 Windows 10 上结束,没有输出。从 运行 到 大约需要 2 秒,这看起来很长,让我觉得 可能有一些错误 ?
然后我查看了 dumpbin 输出:
D:\ASM>dumpbin test.exe /ALL
Microsoft (R) COFF/PE Dumper Version 14.22.27905.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file test.exe
PE signature found
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (x86)
2 number of sections
5E96C000 time date stamp Wed Apr 15 10:04:16 2020
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
102 characteristics
Executable
32 bit word machine
OPTIONAL HEADER VALUES
10B magic # (PE32)
14.22 linker version
200 size of code
200 size of initialized data
0 size of uninitialized data
1000 entry point (00401000)
1000 base of code
2000 base of data
400000 image base (00400000 to 00402FFF)
1000 section alignment
200 file alignment
<further header values omitted ...>
SECTION HEADER #1
.text name
E virtual size
1000 virtual address (00401000 to 0040100D)
200 size of raw data
200 file pointer to raw data (00000200 to 000003FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read
RAW DATA #1
00401000: F4 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 0A ôHello, World.
SECTION HEADER #2
.rdata name
58 virtual size
2000 virtual address (00402000 to 00402057)
200 size of raw data
400 file pointer to raw data (00000400 to 000005FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only
RAW DATA #2
00402000: 00 00 00 00 00 C0 96 5E 00 00 00 00 0D 00 00 00 .....À.^........
00402010: 3C 00 00 00 1C 20 00 00 1C 04 00 00 00 00 00 00 <.... ..........
00402020: 00 10 00 00 0E 00 00 00 2E 74 65 78 74 00 00 00 .........text...
00402030: 00 20 00 00 1C 00 00 00 2E 72 64 61 74 61 00 00 . .......rdata..
00402040: 1C 20 00 00 3C 00 00 00 2E 72 64 61 74 61 24 7A . ..<....rdata$z
00402050: 7A 7A 64 62 67 00 00 00 zzdbg...
Debug Directories
Time Type Size RVA Pointer
-------- ------- -------- -------- --------
5E96C000 coffgrp 3C 0000201C 41C
Summary
1000 .rdata
1000 .text
文件header字段"characteristics"是标志的组合。特别是 102h = 1 0000 0010b
和两个设置标志(根据 PE 格式文档)是 IMAGE_FILE_EXECUTABLE_IMAGE
和 IMAGE_FILE_BYTES_REVERSED_HI
。后者有描述
IMAGE_FILE_BYTES_REVERSED_HI:
Big endian: the MSB precedes the LSB in memory. This flag is deprecated and should be zero.
我问自己:为什么现代汇编器和现代链接器会产生弃用标志?
文件中有 2 个部分。 .text
部分在 ass 中定义bler 代码(并且是唯一包含可执行代码的代码,如其 header 中所指定)。 我不知道第二部分'.rdata'(名称似乎指的是"readable data")是什么或在这里做什么。为什么要创建它?我怎么知道?
反汇编
我用dumpbin反汇编.exe文件(命令dumpbin test.exe /DISASM
)。它获得 hlt
正确,'Hello, World.' 字符串(可能不幸)被解释为可执行命令。我猜这很难归咎于反汇编程序。但是,如果我理解正确(我在汇编编程方面没有实际经验),将数据放入代码段并不是闻所未闻(在研究汇编编程时发现的几个示例中就是这样做的)。 有没有更好的反汇编方法,可以更好地重现我的汇编代码?另外,编译器有时会以这种方式将数据放入代码段吗?
我认为我无法回答所有问题。我也是初学者,所以我可能会说一些不准确的事情。但是,我会尽力而为,我想我可以给你带来一些东西。
不,编译器不会将数据放在代码段中(如果我错了请纠正我)。有 .data 部分(用于初始化数据)和 .bss 部分(用于未初始化数据)。
我想,我最好向您展示一个打印 hello world 的程序示例(对于 linux,因为它更简单,而且我不知道如何使用 windows。在 x64 中,但它就像 x86。只是系统调用的名称和不同的寄存器。x64 用于 64 位,x86 用于 32 位)。
BITS 64 ;not obligatory but I prefer
section .data
msg db "hello world" ;the message
len equ $-msg ;the length of msg
section .text
global _start
_start: ;the entry point
mov rax, 1 ;syscall 1 to print something
mov rdi, 1 ;1 for stdout
mov rsi, msg ;the message
mov rdx, len ;length in rdx
syscall
mov rax, 60 ;exit syscall
mov rdi, 0 ;exit with 0
syscall
(https://tio.run/#assembly-nasm 如果你不想使用 VM。我建议你寻找 WSL + vscode 如果你正在使用 windows。你将有 [= windows 中的 38=] 和 vscode 中的扩展名可以访问 windows 中的文件,但是
如果你想反汇编代码或查看内存是多少,你可以在linux中使用gdb或radare2。对于windows,还有ghidra、IDA、olly dbg等其他工具..
我不知道有什么方法可以让编译器创建更好的汇编代码。但不代表不存在
我从来没有为 windows 做过任何东西。但是,对于 link 我的目标文件,我使用 ld (我不知道它是否有帮助)。
ld object.o -o compiledprogram
我现在没有时间继续写作,所以我现在不能给你任何课程建议。我稍后再看。
希望对您有所帮助。
在某些方面,这是一个非常广泛的问题,可能因此而无法生存。资料全在网上,自己找找看,不复杂,不值得写论文视频。
所以你有一个粗略的想法,即编译器将用一种语言编写的程序转换为另一种语言,可以是汇编语言或机器代码或其他语言。
然后是文件格式,有许多不同的文件格式,我们都使用术语“二进制”来表示不同的格式。理想情况下,它们使用某种形式的编码包含机器代码和数据或有关数据的信息。
暂时打算用ARM,定长指令方便反汇编阅读等等
#define ONE 1
unsigned int x;
unsigned int y = 5;
const unsigned int z = 7;
unsigned int fun ( unsigned int a )
{
return(a+ONE);
}
和gnu gcc/binutils 因为它非常有名,被广泛使用,你可以用它在你的wintel机器上制作程序。我 运行 Linux 所以你会看到 elf 而不是 exe,但这只是你所要求的文件格式。
arm-none-eabi-gcc -O2 -c so.c -save-temps -o so.o
此工具链(linked 的工具链,例如编译器 -> 汇编器 -> linker)是 Unix 风格和模块化的。您将有一个针对目标的汇编器,所以不确定为什么要 re-invent 那个,通过查看汇编输出来调试编译器比尝试直接进入机器代码要容易得多。但是有些人喜欢爬山只是因为它在那里而不是四处走动,一些工具直接用于机器代码只是因为它在那里。
这个特定的编译器有这个保存临时文件的功能,gcc 本身是一个前端程序,它为真正的编译器做准备然后如果被要求(如果你不说不)将调用汇编器并且 link呃
cat so.i
# 1 "so.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "so.c"
unsigned int x;
unsigned int y = 5;
const unsigned int z = 7;
unsigned int fun ( unsigned int a )
{
return(a+1);
}
所以在这一点上定义和包含被处理并且它的一个大文件被发送到编译器。
编译器完成它的工作并将其转换为汇编语言
cat so.s
.cpu arm7tdmi
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 2
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "so.c"
.text
.align 2
.global fun
.arch armv4t
.syntax unified
.arm
.fpu softvfp
.type fun, %function
fun:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
add r0, r0, #1
bx lr
.size fun, .-fun
.global z
.global y
.comm x,4,4
.section .rodata
.align 2
.type z, %object
.size z, 4
z:
.word 7
.data
.align 2
.type y, %object
.size y, 4
y:
.word 5
.ident "GCC: (GNU) 9.3.0"
然后将其放入目标文件中,在本例中为 binutils,linux 默认等
file so.o
so.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped
它使用的是 elf 文件格式,易于查找信息,易于编写程序进行解析等。
我可以反汇编这个,请注意,因为我正在使用反汇编程序,所以它会尝试反汇编所有内容,即使它不是机器代码,坚持使用 32 位 arm 的东西它可以通过它来研究,当有真正的指令时,他们显示(对齐而不是这里使用的可变长度,所以你可以线性反汇编,你不能使用可变长度指令集并有成功的希望(如x86)你需要按执行顺序反汇编然后你经常会错过一些由于根据程序的性质)
arm-none-eabi-objdump -D so.o
so.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <fun>:
0: e2800001 add r0, r0, #1
4: e12fff1e bx lr
Disassembly of section .data:
00000000 <y>:
0: 00000005 andeq r0, r0, r5
Disassembly of section .rodata:
00000000 <z>:
0: 00000007 andeq r0, r0, r7
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 movtmi r4, #14080 ; 0x3700
4: 4728203a ; <UNDEFINED> instruction: 0x4728203a
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e332e39 mrccs 14, 1, r2, cr3, cr9, {1}
10: Address 0x0000000000000010 is out of bounds.
Disassembly of section .ARM.attributes:
00000000 <.ARM.attributes>:
0: 00002941 andeq r2, r0, r1, asr #18
4: 61656100 cmnvs r5, r0, lsl #2
8: 01006962 tsteq r0, r2, ror #18
c: 0000001f andeq r0, r0, pc, lsl r0
10: 00543405 subseq r3, r4, r5, lsl #8
14: 01080206 tsteq r8, r6, lsl #4
18: 04120109 ldreq r0, [r2], #-265 ; 0xfffffef7
1c: 01150114 tsteq r5, r4, lsl r1
20: 01180317 tsteq r8, r7, lsl r3
24: 011a0119 tsteq r10, r9, lsl r1
28: Address 0x0000000000000028 is out of bounds.
是的,该工具在其中放置了额外的东西,但请注意,主要是我创建的。一些代码,一些初始化的 read/write 数据,一些初始化的 read/write 数据和一些初始化的只读数据。工具链作者可以使用他们想要的任何名称,他们甚至不必使用术语部分。但从几十年的历史、通信和术语来看,.text 通常用于代码(如只读机器代码和相关数据),.bss 用于归零 read/write 数据,尽管我看到过其他名称,.data 用于初始化 read/write 数据和此工具的这一代 .rodata 用于只读初始化数据(技术上可以登陆 .text)
请注意,它们的地址均为零。他们还没有 linked。
现在这很丑陋,但为了避免添加更多代码,如果该工具允许我这样做,让我们 link 制作一个完全无法使用的二进制文件(没有 bootstrap,等等) :
arm-none-eabi-ld -Ttext=0x1000 -Tdata=0x2000 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
arm-none-eabi-objdump -D so.elf
so.elf: file format elf32-littlearm
Disassembly of section .text:
00001000 <fun>:
1000: e2800001 add r0, r0, #1
1004: e12fff1e bx lr
Disassembly of section .data:
00002000 <y>:
2000: 00000005 andeq r0, r0, r5
Disassembly of section .rodata:
00001008 <z>:
1008: 00000007 andeq r0, r0, r7
Disassembly of section .bss:
00002004 <x>:
2004: 00000000 andeq r0, r0, r0
现在是 linked。只读项 .text 和 .rodata 以在文件中找到的顺序落在 .text 地址 space 中。 read/write 项按照在文件中找到的顺序落入 .data 地址 space。
是的,对象中的 .bss 在哪里?它就在那里,它没有作为对象一部分的以字节为单位的实际数据,而是具有名称和大小,并且它是 .bss。无论出于何种原因,该工具确实会从 linked 二进制文件中显示它。
回到术语二进制。 so.elf 二进制文件包含内存中组成程序的字节,还有文件格式基础结构加上符号 table 使反汇编和调试更容易以及其他内容。 Elf 是一种灵活的文件格式,gnu 可以使用它,您会得到一个结果,其他工具或工具版本可以使用它并拥有不同的文件。显然,两个编译器可以从同一个源程序生成不同的机器代码,这不仅仅是由于优化,工作是用目标语言制作一个功能程序,功能是 compiler/tool 作者的意见。
内存映像类型文件呢:
arm-none-eabi-objcopy so.elf so.bin -O binary
hexdump -C so.bin
00000000 01 00 80 e2 1e ff 2f e1 07 00 00 00 00 00 00 00 |....../.........|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001000 05 00 00 00 |....|
00001004
现在 objcopy 工具的工作原理是,它从第一个定义的可加载项或任何您想使用的字节开始,以最后一个字节结束,并使用(零)填充来制作文件大小匹配,以便内存映像从地址角度匹配。星号基本上意味着 0 填充。因为我们从 .text 的 0x1000 开始,.data 从 0x2000 开始,但是这个文件的第一个字节(偏移量 0)是 .text 的开始,后面的 0x1000 字节是文件中的偏移量 0x1000 但我们知道它在内存中转到 0x2000是 read/write 的东西。还要注意 bss zeros 不在输出中。 bootstrap 预计为零 those.
没有诸如此文件中的数据在内存中的位置等信息。如果你想一想如果我在我定义的部分有一个字节去 0x00000000 和一个字节在一个部分怎么办我定义到 0x80000000 并输出这个文件,是的,这是一个 0x80000001 字节的文件,尽管相关信息只有两个有用的字节。一个 2GB 的文件,可以容纳两个字节。这就是为什么在整理好 linker 脚本和工具之前不想输出这种文件格式的原因。
相同的数据和另外两种同样老派的格式,有一点英特尔与摩托罗拉的历史
arm-none-eabi-objcopy so.elf so.hex -O ihex
cat so.hex
:08100000010080E21EFF2FE158
:0410080007000000DD
:0420000005000000D7
:0400000300001000E9
:00000001FF
arm-none-eabi-objcopy so.elf so.srec -O srec
cat so.srec
S00A0000736F2E7372656338
S10B1000010080E21EFF2FE154
S107100807000000D9
S107200005000000D3
S9031000EC
现在这些包含相关字节,加上地址,但没有太多其他信息,每个数据字节需要两个字节以上,但与带有填充的巨大文件相比,值得 trade-off。这两种格式今天都在使用,虽然不如过去那么多,但仍然存在。
无数其他二进制文件格式和像 objdump 这样的工具有一个不错的格式列表,它可以生成以及其他 linkers and/or 工具。
与所有这些相关的是存在某种形式的二进制文件格式,其中包含我们 运行 程序所需的字节。
您可能会问什么格式和什么地址...那是操作系统或系统设计的一部分。在 Windows 的情况下,windows 操作系统(您使用的特定版本)可能支持特定的文件格式和 ose 格式的变体。 Windows 已经确定了地址 space 的样子。像这样的操作系统利用 MMU 来虚拟化地址和保护。拥有一个虚拟地址 space 意味着每个程序都可以住在同一个 space 中。例如,所有程序都可以有一个从零开始的地址....
test.c
int main ( void )
{
return 1;
}
hello.c
int main ( void )
{
return 2;
}
gcc test.c -o test
objdump -D test
Disassembly of section .text:
00000000004003e0 <_start>:
4003e0: 31 ed xor %ebp,%ebp
4003e2: 49 89 d1 mov %rdx,%r9
4003e5: 5e pop %rsi
...
gcc hello.c -o hello
objdump -D hello
Disassembly of section .text:
00000000004003e0 <_start>:
4003e0: 31 ed xor %ebp,%ebp
4003e2: 49 89 d1 mov %rdx,%r9
同一个地址,possible 怎么会不会叠在一起?没有虚拟机。请注意,这是为特定日期的特定 Linux 构建的,等等。工具链有一个默认的 linker 脚本(注意我没有指定如何 link)这个平台当编译器为此构建时 target/platform.
arm-none-eabi-gcc -O2 test.c -c -o test.o
arm-none-eabi-ld test.o -o test.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000008000
arm-none-eabi-objdump -D test.elf
test.elf: file format elf32-littlearm
Disassembly of section .text:
00008000 <main>:
8000: e3a00001 mov r0, #1
8004: e12fff1e bx lr
相同的源代码,相同的编译器,为不同的目标和系统不同的地址构建。
所以对于 Windows 肯定会有支持的二进制格式的规则和可以使用的地址 spaces 的规则,如何定义 ose spaces 在文件中。
然后操作系统启动器读取二进制文件并将可加载项目放入内存中的ose地址(在虚拟space中os 已经为这个特定程序创建了)非常 pos 加载程序的一个功能是为您将 bss 归零,因为信息在那里。低级程序员需要知道 pos 巧妙地处理 .bss 与否。
如果没有,您将看到并且可能需要创建一个解决方案,不幸的是,这是您深入了解工具特定项目的地方。虽然 C 可能在某种程度上是标准化的,但有一些工具特定的东西没有被 tool/authors 标准化,或者至少被 tool/authors 标准化,但没有理由假设 those cross 转移到其他工具。
.globl _start
_start:
ldr sp,sp_init
bl fun
b .
.word __bss_start__
.word __bss_end__
sp_init:
.word 0x8000
关于汇编语言的一切都是特定于工具的,出于理智的原因,助记符无疑会类似于 ip/processor 供应商文档,这些文档使用他们付费开发的工具所使用的语法。但除此之外,汇编语言完全由工具而不是目标定义,x86 因为它的年龄和其他事情真的很糟糕,这不是英特尔与 AT&T 的事情,只是一般而言。 Gnu 汇编程序是众所周知的,因为我认为可能有意不使语言与其他汇编语言兼容。以上是arm的gnu汇编器。
使用上面的 fun() 函数,C 说它应该是 main() 但工具不在乎我已经在这里输入了足够的内容。
添加一个简单的基于 ram 的 linker 脚本
MEMORY
{
ram : ORIGIN = 0x1000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > ram
.rodata : { *(.rodata*) } > ram
.bss : {
__bss_start__ = .;
*(.bss*)
} > ram
__bss_end__ = .;
}
打造一切
arm-none-eabi-as start.s -o start.o
arm-none-eabi-gcc -O2 -c so.c -o so.o
arm-none-eabi-ld -T sram.ld start.o so.o -o so.elf
检查
arm-none-eabi-nm so.elf
0000102c B __bss_end__
00001028 B __bss_start__
00001018 T fun
00001014 t sp_init
00001000 T _start
00001028 B x
00001024 D y
00001020 R z
arm-none-eabi-objdump -D so.elf
so.elf: file format elf32-littlearm
Disassembly of section .text:
00001000 <_start>:
1000: e59fd00c ldr sp, [pc, #12] ; 1014 <sp_init>
1004: eb000003 bl 1018 <fun>
1008: eafffffe b 1008 <_start+0x8>
100c: 00001028 andeq r1, r0, r8, lsr #32
1010: 0000102c andeq r1, r0, r12, lsr #32
00001014 <sp_init>:
1014: 00008000 andeq r8, r0, r0
00001018 <fun>:
1018: e2800001 add r0, r0, #1
101c: e12fff1e bx lr
Disassembly of section .rodata:
00001020 <z>:
1020: 00000007 andeq r0, r0, r7
Disassembly of section .data:
00001024 <y>:
1024: 00000005 andeq r0, r0, r5
Disassembly of section .bss:
00001028 <x>:
1028: 00000000 andeq r0, r0, r0
所以现在 possible 添加到 bootstrap 内存归零循环(不要使用 C/memset 你不会产生先有鸡还是先有蛋的问题 你写bootstrap in asm) 基于开始和结束地址。
幸运或不幸,因为 linker 脚本是工具特定的和汇编语言它是特定于工具的,如果您让这些工具为您完成工作,它们需要一起工作(理智的方法,找出 .bss 在哪里,否则会很有趣)。
这可以在操作系统上完成,但是当你进入微控制器时,所有这些都必须在 non-volatile 存储(闪存)上,那么 pos 可能有一个从其他地方(有时像你的鼠标固件,有时是键盘等)下载到 ram,假设是闪存,那么你如何处理 .data?
MEMORY
{
rom : ORIGIN = 0x0000, LENGTH = 0x1000
ram : ORIGIN = 0x1000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > rom
.rodata : { *(.rodata*) } > rom
.data : {
*(.data*)
} > ram AT > rom
.bss : {
__bss_start__ = .;
*(.bss*)
} > ram
__bss_end__ = .;
}
对于 gnu ld,这基本上表示 .data 的主目录在 ram 中,但输出二进制格式会将其放入 flash/rom
so.elf so.srec -O srec
cat so.srec
S00A0000736F2E7372656338
S11300000CD09FE5030000EBFEFFFFEA04100000A4
S11300100810000000800000010080E21EFF2FE1B4
S107002007000000D1 <- z variable at address 0020
S107002405000000CF <- y variable at 0024
S9030000FC
并且您必须更多地使用 linker 脚本来获得告诉您 ram 和闪存起始地址和结束地址或长度的工具。然后在 bootstrap(asm 不是 C)中添加代码以将 .data 从闪存复制到 ram。
另请注意您的众多问题中的另一个问题。
.word __bss_start__
.word __bss_end__
sp_init:
.word 0x8000
这些项目是技术数据。但它们首先存在于 .text 中,然后存在于 foremost 中,因为它们是在假定为 .text 的代码中定义的(我不需要在 asm 中说明这一点,但可以有)。你也会在 x86 中看到这个,但是对于像 arm、mips、risc-v 等固定长度,你不能在指令本身中放置任何你想要的旧 immediate/constant/linked 值,你将它放在附近的“池”并进行 pc 相关读取以获取它。对于 linking 外部设备,您也会看到这一点:
extern unsigned int x;
int main ( void )
{
return x;
}
arm-none-eabi-gcc -O2 -c test.c -o test.o
arm-none-eabi-objdump -D test.o
test.o: file format elf32-littlearm
Disassembly of section .text.startup:
00000000 <main>:
0: e59f3004 ldr r3, [pc, #4] ; c <main+0xc>
4: e5930000 ldr r0, [r3]
8: e12fff1e bx lr
c: 00000000 andeq r0, r0, r0 <--- the code gets the address of the
从这里变量然后从内存中读取它
一次 linked
Disassembly of section .text:
00008000 <main>:
8000: e59f3004 ldr r3, [pc, #4] ; 800c <main+0xc>
8004: e5930000 ldr r0, [r3]
8008: e12fff1e bx lr
800c: 00018010 andeq r8, r1, r0, lsl r0
Disassembly of section .data:
00018010 <x>:
18010: 00000005 andeq r0, r0, r5
对于 x86
gcc -c -O2 test.c -o test.o
dwelch-desktop so # objdump -D test.o
test.o: file format elf64-x86-64
Disassembly of section .text.startup:
0000000000000000 <main>:
0: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 6 <main+0x6>
6: c3 retq
00000000004003e0 <main>:
4003e0: 8b 05 4a 0c 20 00 mov 0x200c4a(%rip),%eax # 601030 <x>
4003e6: c3 retq
如果你眯着眼睛真的不一样吗?处理器读取附近的数据以加载到寄存器和/或使用。无论哪种方式,由于指令集的性质,linker 修改指令或附近的池数据或两者。
最后一个:
arm-none-eabi-gcc -S test.c
cat test.s
.cpu arm7tdmi
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 6
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "test.c"
.text
.align 2
.global main
.arch armv4t
.syntax unified
.arm
.fpu softvfp
.type main, %function
main:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
str fp, [sp, #-4]!
add fp, sp, #0
ldr r3, .L3
ldr r3, [r3]
mov r0, r3
add sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx lr
.L4:
.align 2
.L3:
.word x
.size main, .-main
.ident "GCC: (GNU) 9.3.0"
所以你能看到汇编语言吗,是的,有些工具会让你保存中间文件and/or让你在编译时生成文件的汇编输出。
你能在代码中有数据吗,是的,有时和有理由在 .text 区域中有数据值,而不仅仅是目标特定你会出于各种原因看到这个,一些工具链把只读数据放在那里。
有许多文件格式,现代操作系统使用的文件格式不仅具有定义构成机器代码和数据值的字节的功能,而且还包括符号和其他调试信息。
程序的文件格式和内存 space 是操作系统特定的,而不是语言甚至目标特定的(Linux、Windows、同一台笔记本电脑上的 MacOS 预计不会尽管目标计算机完全相同,但规则相同)。该平台的本机工具链具有默认的 linker 脚本以及为该目标构建 usable/working 程序所需的任何其他信息。包括支持的文件格式。
机器代码和数据项可以以不同的方式以不同的文件格式表示,目标系统的操作系统或加载程序是否可以使用该格式取决于目标系统。
程序有错误和细微差别。文件格式有版本和不一致,你可能会发现一些 elf 文件格式 reader 只是发现它不起作用或在提供一个在某些系统上工作的非常好的 elf 文件时打印出奇怪的东西。为什么要设置一些标志?也许 those 字节得到了 re-used 或标志重新利用 osed 或数据结构改变或工具以不同的方式或以 non-standard 方式使用它(想想 mov 20h,ax) 和另一个不兼容的工具无法理解或幸运地获得足够的 close。
在 Stack Overflow 上问“为什么”问题不是很有用,找到写这东西的人的几率非常低,询问你从哪里获得工具并跟随希望这个人是的人的几率更高还活着,愿意被打扰。并且 99.999(很多 9)% 没有一套全球性的神圣规则来写这件事 under/for。总的来说,有些家伙只是觉得这就是为什么他们做了他们所做的,没有真正的原因,懒惰,一个错误,故意试图破坏别人的工具。一直到有意见的大型委员会在特定的一天在特定的房间对其进行投票,这就是原因(我们知道当我们由委员会设计或尝试编写没有人遵守的规范时我们会得到什么)。
我知道你在 Windows,我手头没有 Windows 机器,我在 Linux。但是 gnu/binutils 和 clang/llvm 工具很容易获得,并且有一套丰富的工具,如 readelf、nm、objdump 等。这些工具有助于检查事物,一个好的工具至少在内部会有这样的工具对于开发人员来说,他们可以将工具的输出调试到一定的质量水平。 gnu 人制作了工具并让每个人都可以使用它们,虽然这需要时间通过它们和它们的特性,它们对于你想要理解的东西非常强大。
你不会找到一个好的 x86 反汇编器,它们都是垃圾,仅仅是因为野兽的本性。它是一个可变长度的指令集,所以根据定义,除非你正在执行,否则你不能正确地整理它。您必须按执行顺序从一个已知的良好入口点进行反汇编,才有一半的机会,然后由于各种原因,您无法以这种方式看到代码路径(例如,想想跳转 tables,或 dll 或 so 文件) .最好的解决方案是拥有非常 accurate/perfect emulator/simulator 和 运行 的代码,并执行您需要做的所有 actions/gyrations 以使其覆盖所有代码路径,并且让该工具记录来自数据的指令以及每个指令所在的位置或每个没有分支的线性部分。
这样做的好处是,今天很多代码都是使用没有试图隐藏任何东西的工具编译的。在过去,由于各种原因,您会看到手写的 asm,故意试图防止反汇编或由于其他因素(在贸易展前一天为视频游戏手工编辑二进制 rom 图像,去拆解一些经典的 rom) .
mov r0,#0
cmp r0,#0
jz somewhere
.word 0x12345678
反汇编程序无法解决这个问题,有些人可能会为此添加案例
mov r0,#0
nop
nop
xor r0,#1
nop
nop
xor r0,#3
xor r0,#2
cmp r0,#0
jz somewhere
.word 0x12345678
并且它认为数据是一条指令,因为可变长度对于反汇编程序来说很难解决一个像样的指令,至少会检测到指令的非操作码部分分支到 and/or 的冲突指令的操作码部分稍后显示为其他指令中的附加字节。该工具无法解决人类必须解决的问题。
即使使用 arm 和 mips 并具有 32 位和 16 位指令,risc-v 具有可变大小的指令等...
gnu 的反汇编程序经常会被 x86 绊倒。
文中问题的答案: 1、可以看到进程一步步执行,用debugger可以看到进程内存。我使用 OllyDbg 来学习汇编,它是免费且功能强大的调试器。 2. 进程在调用 NtCreateUserProcess 后由 Windows 内核加载,所以我认为您需要内核调试才能了解它是如何完成的。 3. OllyDbg中调试的代码自动反汇编。 4.您可以将只读数据放在“.text”部分。您可以更改节标志使其可写,然后可以混合代码和数据。一些编译器可能会合并“.text”和“.rdata”部分。
我建议您按顺序阅读 PE 导入、导出、迁移和资源。如果您想查看最简单的 i386 PE helloworld,您可以在此处查看我的 hello_world_pe_i386_dynamic.exe 程序:https://github.com/pajacol/hello-world。我完全是用二进制文件编辑器写的。它只包含必需的数据结构。此可执行文件与位置无关,可以在任何地址加载而无需重定位。