在这个附录中我将给大家展示一下怎样手工建立一个程序。因为我不会DEC Alpha,本例将使用Intel汇编语言。
本程序相当于
#include <stdio.h>
int main(void)
{
puts(hello,world);
return 0;
}
首先,我使用Win32函数来翻译它以取代C运行时库:
#define STD_OUTPUT_HANDLE -11UL
#define hello "hello, world\n"
__declspec(dllimport) unsigned long __stdcall
GetStdHandle(unsigned long hdl);
__declspec(dllimport) unsigned long __stdcall
WriteConsoleA(unsigned long hConsoleOutput,
const void *buffer,
unsigned long chrs,
unsigned long *written,
unsigned long unused
);
static unsigned long written;
void startup(void)
{
WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE),hello,sizeof(hello)-1,&written,0);
return;
}
现在我将笨拙的将它汇编出来:
startup:
; parameters for WriteConsole(), backwards
6A 00 push 0x00000000
68 ?? ?? ?? ?? push offset _written
6A 0D push 0x0000000d
68 ?? ?? ?? ?? push offset hello
; parameter for GetStdHandle()
6A F5 push 0xfffffff5
2E FF 15 ?? ?? ?? ?? call dword ptr cs:__imp__GetStdHandle@4
; result is last parameter for WriteConsole()
50 push eax
2E FF 15 ?? ?? ?? ?? call dword ptr cs:__imp__WriteConsoleA@20
C3 ret
hello:
68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A "hello, world\n"
_written:
00 00 00 00
以上就是编译的部分。任何人都能做到这点。从现在起让我们扮演起链接器的角色,这会非常有趣 :-)
我需要先找出函数WriteConsoleA()和GetStdHandle()。碰巧它们都在“kernel32.dll”中。(这是“输入库”部分。)
现在我开始做可执行文件。问号代表待定的值;它们将在以后被修正。
首先是DOS-根,开始于0x0,有0x40字节长:
00 | 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30 | 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
正如你所见到的,这不是真正的MS-DOS程序。它只是一个开始部分有“MZ”签名的头和紧跟在头后面的e_lfanew指针,没有任何代码。这是因为它并非打算运行于MS-DOS之上;它之所以在这里只是因为规范的需要。
然后是PE签名,开始于0x40,有0x4字节长:
50 45 00 00
现在到了文件头,开始于0x44,有0x14字节长:
Machine 4c 01 ; i386
NumberOfSections 02 00 ; 代码段和数据段
TimeDateStamp 00 00 00 00 ; 谁管它?
PointerToSymbolTable 00 00 00 00 ; 未用
NumberOfSymbols 00 00 00 00 ; 未用
SizeOfOptionalHeader e0 00 ; 常量
Characteristics 02 01 ; 32位机器上的可执行文件
接着是可选头,开始于0x58,有0x60字节长:
Magic 0b 01 ; 常量
MajorLinkerVersion 00 ; 我是 0.0 版:-)
MinorLinkerVersion 00 ;
SizeOfCode 20 00 00 00 ; 32字节代码
SizeOfInitializedData ?? ?? ?? ?? ; 待找出
SizeOfUninitializedData 00 00 00 00 ; 我们没有BSS节
AddressOfEntryPoint ?? ?? ?? ?? ; 待定
BaseOfCode ?? ?? ?? ?? ; 待定
BaseOfData ?? ?? ?? ?? ; 待定
ImageBase 00 00 10 00 ; 1 MB, 随意选
SectionAlignment 20 00 00 00 ; 32字节对齐
FileAlignment 20 00 00 00 ; 32字节对齐
MajorOperatingSystemVersion 04 00 ; NT 4.0
MinorOperatingSystemVersion 00 00 ;
MajorImageVersion 00 00 ;0.0版
MinorImageVersion 00 00 ;
MajorSubsystemVersion 04 00 ; Win32 4.0
MinorSubsystemVersion 00 00 ;
Win32VersionValue 00 00 00 00 ; 未使用?
SizeOfImage ?? ?? ?? ?? ; 待定
SizeOfHeaders ?? ?? ?? ?? ; 待定
CheckSum 00 00 00 00 ; 非驱动不用
Subsystem 03 00 ; Win32控制台
DllCharacteristics 00 00 ; 未用 (不是一个DLL)
SizeOfStackReserve 00 00 10 00 ; 1 MB栈
SizeOfStackCommit 00 10 00 00 ; 开始时4 KB
SizeOfHeapReserve 00 00 10 00 ; 1 MB堆
SizeOfHeapCommit 00 10 00 00 ; 开始时4 KB
LoaderFlags 00 00 00 00 ; 未知
NumberOfRvaAndSizes 10 00 00 00 ; 常量
正如你所见,我计划只用2个节,一个用于代码,一个用于所有剩余的东西(数据、常量和输入目录等)。没有重定位和象资源之类其它东西。我也不用BSS节并将变量“written”放入已初始化数据。文件和RAM中的节对齐都是一样的(32字节);这将有助于使任务简单,否则我就得来回地计算RVA很多次。
现在我们设置数据目录,开始于0xb8字节,有 0x80字节长:
地址 大小
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
?? ?? ?? ?? ?? ?? ?? ?? ; IMAGE_DIRECTORY_ENTRY_IMPORT (1)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_SECURITY (4)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_DEBUG (6)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_TLS (9)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_IAT (12)
00 00 00 00 00 00 00 00 ; 13
00 00 00 00 00 00 00 00 ; 14
00 00 00 00 00 00 00 00 ; 15
仅使用输入目录。
下一个使节头。首先我们做代码节的,代码节将包含前面所编的汇编语句。它有32字节长,所以代码节也就是这么长。节头从0x138处开始,有0x28字节长:
Name 2e 63 6f 64 65 00 00 00 ; ".code"的ASCII码值
VirtualSize 00 00 00 00 ; 未用
VirtualAddress ?? ?? ?? ?? ; 待定
SizeOfRawData 20 00 00 00 ; 代码的大小
PointerToRawData ?? ?? ?? ?? ; 待定
PointerToRelocations 00 00 00 00 ; 未用
PointerToLinenumbers 00 00 00 00 ; 未用
NumberOfRelocations 00 00 ; 未用
NumberOfLinenumbers 00 00 ; 未用
Characteristics 20 00 00 60 ; 代码节,可执行,可读
第二节将包含数据。节头开始于0x160处,有0x28字节长:
Name 2e 64 61 74 61 00 00 00 ; ".data"的ASCII码值
VirtualSize 00 00 00 00 ; 未用
VirtualAddress ?? ?? ?? ?? ; 待定
SizeOfRawData ?? ?? ?? ?? ; 待定
PointerToRawData ?? ?? ?? ?? ; 待定
PointerToRelocations 00 00 00 00 ; 未用
PointerToLinenumbers 00 00 00 00 ; 未用
NumberOfRelocations 00 00 ; 未用
NumberOfLinenumbers 00 00 ; 未用
Characteristics 40 00 00 c0 ; 已初始化的,可读,可写
下一个字节位于0x188处,但节需要按32字节(的倍数)对齐(因为我是这样选择的),所以我们需要添一些(0)字节直到0x1a0处:
00 00 00 00 00 00 ; 填充的
00 00 00 00 00 00
00 00 00 00 00 00
00 00 00 00 00 00
现在第一节,就是上面所汇编的代码节,“到”了。它开始于0x1a0处,有0x20字节长:
6A 00 ; push 0x00000000
68 ?? ?? ?? ?? ; push offset _written
6A 0D ; push 0x0000000d
68 ?? ?? ?? ?? ; push offset hello_string
6A F5 ; push 0xfffffff5
2E FF 15 ?? ?? ?? ?? ; call dword ptr cs:__imp__GetStdHandle@4
50 ; push eax
2E FF 15 ?? ?? ?? ?? ; call dword ptr cs:__imp__WriteConsoleA@20
C3 ; ret
因为这一节的长度(刚好32字节),在下一节(数据节)前我们不需要填充任何字节。下一节到了,从0x1c0处开始:
68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A ; "hello, world\n"的ASCII码值
00 00 00 ; 填充几个0以和_written对齐
00 00 00 00 ; _written
现在剩下的只有输入目录了。本文件将从"kernel32.dll"库中输入2个函数,输入目录将从本节的变量后面立即开始。首先我们先将上面的数据按32字节对齐:
00 00 00 00 00 00 00 00 00 00 00 00 ; 填充的
在0x1e0处开始输入描述(IMAGE_IMPORT_DESCRIPTOR):
OriginalFirstThunk ?? ?? ?? ?? ; 待定
TimeDateStamp 00 00 00 00 ; 未绑定
ForwarderChain ff ff ff ff ; 无中转
Name ?? ?? ?? ?? ; 待定
FirstThunk ?? ?? ?? ?? ; 待定
我们需要用一个0字节项来结束输入目录(我们现在位于0x1f4):
OriginalFirstThunk 00 00 00 00 ; 结束符号
TimeDateStamp 00 00 00 00 ;
ForwarderChain 00 00 00 00 ;
Name 00 00 00 00 ;
FirstThunk 00 00 00 00 ;
现在只剩下DLL名字,还有2个换长,以及换长数据和函数名字了。但现在我们真的很快就要完成了。
DLL名字,以0结尾,开始于0x208处:
6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00 ; "kernel32.dll"的ASCII码值
00 00 00 ; 填充到32位边界
原始第一个换长,开始于0x218处:
AddressOfData ?? ?? ?? ?? ; "WriteConsoleA"函数名的RVA
AddressOfData ?? ?? ?? ?? ; "GetStdHandle"函数名的RVA
00 00 00 00 ; 结束符号
第一个换长就是同样的列表,开始于0x224处:
(__imp__WriteConsoleA@20, at 0x224)
AddressOfData ?? ?? ?? ?? ; "WriteConsoleA"函数名的RVA
(__imp__GetStdHandle@4, at 0x228)
AddressOfData ?? ?? ?? ?? ; "GetStdHandle"函数名的RVA
00 00 00 00 ; 结束符号
现在剩下的只有输入名字(IMAGE_IMPORT_BY_NAME)形式的两个函数名了。我们现处于0x230字节。
01 00 ; 序数,不需要正确
57 72 69 74 65 43 6f 6e 73 6f 6c 65 41 00 ; "WriteConsoleA"的ASCII码值
02 00 ; 序数,不需要正确
47 65 74 53 74 64 48 61 6e 64 6c 65 00 ; "GetStdHandle"的ASCII码值
Ok, 这就全部结束了。下一个字节,我们并不真正需要,是0x24f。我们必须将节填充到0x260处:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; 填充的
00
------------
我们已经完成了。因为我们已经知道了所有的字节偏移量,我们可以应用我们的修正到所有原先被用“??”符号标为“未知”的地址和大小了。
我将不强迫你一步一步地去读它(很好懂的),只直接给出结果来:
------------
DOS-头, 开始于0x0:
00 | 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30 | 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
签名, 开始于0x40:
50 45 00 00
文件头, 开始于0x44:
Machine 4c 01 ; i386
NumberOfSections 02 00 ; 代码和数据
TimeDateStamp 00 00 00 00 ; 谁管它?
PointerToSymbolTable 00 00 00 00 ; 未用
NumberOfSymbols 00 00 00 00 ; 未用
SizeOfOptionalHeader e0 00 ; 常量
Characteristics 02 01 ; 可执行于32位机器上
可选头, 开始于0x58:
Magic 0b 01 ; 常量
MajorLinkerVersion 00 ; 我是 0.0版 :-)
MinorLinkerVersion 00 ;
SizeOfCode 20 00 00 00 ; 32字节代码
SizeOfInitializedData a0 00 00 00 ; 数据节大小
SizeOfUninitializedData 00 00 00 00 ; 我们没有 BSS节
AddressOfEntryPoint a0 01 00 00 ; 代码节的开始处
BaseOfCode a0 01 00 00 ; 代码节的RVA
BaseOfData c0 01 00 00 ; 数据节的RVA
ImageBase 00 00 10 00 ; 1 MB, 任意选择
SectionAlignment 20 00 00 00 ; 32字节对齐
FileAlignment 20 00 00 00 ; 32字节对齐
MajorOperatingSystemVersion 04 00 ; NT 4.0
MinorOperatingSystemVersion 00 00 ;
MajorImageVersion 00 00 ; 0.0版本
MinorImageVersion 00 00 ;
MajorSubsystemVersion 04 00 ; Win32 4.0
MinorSubsystemVersion 00 00 ;
Win32VersionValue 00 00 00 00 ; 未用?
SizeOfImage c0 00 00 00 ; 所有节大小的总数
SizeOfHeaders a0 01 00 00 ; 第一节的偏移量
CheckSum 00 00 00 00 ; 非驱动程序不须用
Subsystem 03 00 ; Win32控制台程序
DllCharacteristics 00 00 ; 未用(不是一个DLL)
SizeOfStackReserve 00 00 10 00 ; 1 MB 栈
SizeOfStackCommit 00 10 00 00 ; 开始时4 KB
SizeOfHeapReserve 00 00 10 00 ; 1 MB 堆
SizeOfHeapCommit 00 10 00 00 ; 开始时4 KB
LoaderFlags 00 00 00 00 ; 未知
NumberOfRvaAndSizes 10 00 00 00 ; 常量
数据目录,开始于 0xb8:
地址 大小
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
e0 01 00 00 6f 00 00 00 ; IMAGE_DIRECTORY_ENTRY_IMPORT (1)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
00 00 00 00 00 00 00 00 ; IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
00 00