牛X的Hello World!
时间:2007-05-20 来源:souldemo
我在论坛里问过如何写机器码。我希望能在linux下找个类似于windows中DEBUG的东西
而不是像我这样自己打文件头。可是被人耻笑说我脸计算机原理都不懂,我真的很伤心。
我一直信奉:The limit, forever is remains for these ignorant people。
(限制,永远是留给那些无知的人的。)
我承认他懂的比我多,但是他不该这么说我,这里我还是要重申,我知道linux不是windows,
但是没有DEBUG,我还是能写出来,就是麻烦点。
想编写可执行程序,就得了解ELF。
在研究下ELF文件头结构之前先看下32位平台的几个数据类型:
/* 32-bit ELF base types. */
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
#define EI_NIDENT 16
typedef struct elf32_hdr{
/** 魔数和一些平台版本信息。 前四个字节必须是0x454c46,
[4]表示平台位数, 1表示32位, 2表示64位。
[5]表示数据编码格式,1 表示小尾,2表示大尾。
[6]指定ELF头部版本,目前使用1,
这三位的完整取值可见/usr/include/linux/elf.h
中的定义。
[7]-[16]为填充,为0。
**/
unsigned char e_ident[EI_NIDENT]; /**魔数相关信息 **/
Elf32_Half e_type; /** 文件类型 **/
Elf32_Half e_machine; /** 硬件平台 **/
Elf32_Word e_version; /** 文件版本 **/
Elf32_Addr e_entry; /** 程序入口点 **/
Elf32_Off e_phoff; /** 程序头表偏移量 **/
Elf32_Off e_shoff; /** 节头表偏移量 **/
Elf32_Word e_flags; /** 处理器特定标志 **/
Elf32_Half e_ehsize; /** ELF头长度,即此结构长度, 52 **/
Elf32_Half e_phentsize; /** 程序头表中一个条目长度 **/
Elf32_Half e_phnum; /** 程序头表条目个数 **/
Elf32_Half e_shentsize; /** 节头表中一个条目长度 **/
Elf32_Half e_shnum; /** 节头表条目个数 **/
Elf32_Half e_shstrndx; /** 节头表字符串表索引 **/
} Elf32_Ehdr;
可以通过readelf -h 程序明查看可执行程序的ELF头,下面是个典型输出:
[souldump@localhost bin]$ readelf -h cpuid
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: Intel 80386
Version: 0x1
Entry point address: 0x8048074
Start of program headers: 52 (bytes into file)
Start of section headers: 252 (bytes into file)
Flags: 0x0
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: 6
Section header string table index: 3
ELF头结构之后跟随的是个程序头数组,从程序执行的角度分析,ELF文件被分为很多的段。
分别保存数据,指令,已初始化的数据。
typedef struct elf32_phdr{
Elf32_Word p_type; /** 段类型 **/
Elf32_Off p_offset; /** 段位置相对于文件开始的位移 **/
Elf32_Addr p_vaddr; /** 段在虚拟内存中的地址 **/
Elf32_Addr p_paddr; /** 段在物理内存中的地址 **/
Elf32_Word p_filesz; /** 段在文件中长度 **/
Elf32_Word p_memsz; /** 段在内存中长度 **/
Elf32_Word p_flags; /** 段标志 **/
Elf32_Word p_align; /** 段在内存中对齐标志 **/
} Elf32_Phdr;
查看cpuid程序的输出:
[souldump@localhost bin]$ readelf -l cpuid
Elf file type is EXEC (Executable file)
Entry point 0x8048074
There are 2 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x000ab 0x000ab R E 0x1000
LOAD 0x0000ac 0x080490ac 0x080490ac 0x00029 0x00029 RW 0x1000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
由于这是个由汇编语言编写的代码,没有调用GLIBC的库函数,因此它不需要解释器,不用加载
/lib/ld-linux.so.2.
下面是一个由C编译的程序的部分输出,如果想了解更详细的内容可以自己查看:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00464 0x00464 R E 0x1000
LOAD 0x000464 0x08049464 0x08049464 0x00100 0x00104 RW 0x1000
DYNAMIC 0x000478 0x08049478 0x08049478 0x000c8 0x000c8 RW 0x4
NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
程序头后面跟随的是节头数组:
typedef struct {
Elf32_Word sh_name; /** 小节名,在字符串表中索引 **/
Elf32_Word sh_type; /** 小节类型 **/
Elf32_Word sh_flags; /** 小节属性 **/
Elf32_Addr sh_addr; /** 小节在运行中的虚拟地址 **/
Elf32_Off sh_offset; /** 小节的文件偏移 **/
Elf32_Word sh_size; /** 小节的大小,字节为单位 **/
Elf32_Word sh_link; /** 链接另外一小节的索引 **/
Elf32_Word sh_info; /** 附加的小节信息 **/
Elf32_Word sh_addralign; /** 小节的对齐 **/
/** 如果一个小节保存着一张入定大小入口的表,就是入口的大小 否则为0 **/
Elf32_Word sh_entsize;
} Elf32_Shdr;
通常的C程序如果没使用其他共享库,会有27个section header.如果使用了其他的共享库,
则会按每个共享库一个条目(连接器ld-linux.so.2不算)。
比如/usr/bin/lsattr,由于使用了
0: libe2p.so.2 2007-03-22T05:13:36 0xda8e15c5 0 0
1: libcom_err.so.2 2007-03-22T05:12:10 0x805a324f 0 0
2: libc.so.6 2007-03-22T05:12:01 0x9e4b904b 0 0
3: /lib/ld-linux.so.2 2007-03-22T05:12:01 0x86c26626 0 0
因此会增加3个条目。
根据我对汇编语言编写的可执行代码分析,于C的方式很有很大区别。
使用汇编编译的程序,data节的数据,字符串末尾并不会添加"\0",输出到哪要靠用户自己控制,
而C编译的程序则会在字符串间添加.用C的程序data节的数据是按4字节对齐,但是,汇编中字符
串和数据是挨着排放的,后面直接安放了shstrtab节的数据。程序中定义的变量在函数入口开始
之前排放,就是在.text段开始处分配,而函数的入口则在占用的空间之后。比如.text段加载地址
000000,而我们定义了两个int型的变量,那么函数的实际入口应该为000008,(32位平台int为4字节
同样,如果增加一个字符串,那么也会增加一个指针的偏移(相关节,头的偏移和值也得修改)
你可以很简单的通过readelf程序的输出值比较得到结果。
从程序执行的角度来看,section头不是必须的,而从编译角度来看,程序头也不是必须的。
为了简单起见。我就没要section头,而且为了扩展方便,修改了代码段与数据段的位置。
这是我写出来的Hello World!程序,(虽然确很简单,但还是能说明问题):
用hexedit 16进制文本编辑器,编辑的。
最核心的应该算是那几条指令编码:
eax 的寄存器选择位为0, ebx的为3, 根据mov指令中的:
B8+ rd MOV r32, imm32 Move imm32 to r32.
以下两句:
movl $4, %eax
movl $1, %ebx
汇编为:
B8 04 00 00 00
BB 01 00 00 00
ECX的寄存器选择位为1.edx为2,第一句使用立即数寻址,虽然这是个变量,但是他存储的是地址。
从程序执行的角度来看,他存储的是字符串的的基地址。
所以下面两句:
movl $output, %ecx //随便取的名字。
movl $13, %edx
汇编为:
B9 XX XX XX XX (这里是字符串的首地址)
BA 0D 00 00 00
这是int指令的描述:
CD ib INT imm8 Interrupt vector number specified by immediate byte.
这句
int $0x80
被汇编为:
CD 80 (这里是一个字节啊,不是4个!)
movl $1, %eax
movl $0, %ebx
int $0x80
很容易写出还是上面的:
B8 01 00 00 00
BB 00 00 00 00
CD 80 00 00 //最后这俩是填充的。
/现在,这个.text节应该完成了:
当然了,可以直接写汇编的,然后编译后,然后在用hexedit编辑,把
指令拷贝出来,然后再修改地址。
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
02 00 03 00 01 00 00 00 84 80 04 08 34 00 00 00
74 00 00 00 00 00 00 00 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 80 04 08
00 80 04 08 A5 00 00 00 A5 00 00 00 05 00 00 00
00 01 00 00 01 00 00 00 74 00 00 00 74 90 04 08
74 90 04 08 0D 00 00 00 0D 00 00 00 06 00 00 00
00 01 00 00 48 65 6C 6C 6F 20 57 6F 72 6C 64 21
0A 00 00 00 B8 04 00 00 00 BB 01 00 00 00 B9 74
90 04 08 BA 0C 00 00 00 CD 80 B8 01 00 00 00 BB
00 00 00 00 CD 80 00 00
这个是我从上面的代码中分离的模板,认为可以做个程序通用部分,(代码可数据部分自己去写)
注:前面的是文件偏移量。
00000000 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
00000010 02 00 03 00 01 00 00 00 HH HH HH HH 34 00 00 00
00000020 74 00 00 00 00 00 00 00 34 00 20 00 02 00 00 00
00000030 00 00 00 00
01 00 00 00 00 00 00 00 00 80 04 08
00000040 00 80 04 08 II II II II II II II II 05 00 00 00
00000050 00 01 00 00
01 00 00 00 JJ JJ JJ JJ KK 90 04 08
00000060 KK 90 04 08 LL LL LL LL LL LL LL LL 06 00 00 00
00000070 00 01 00 00
48 65 6C 6C 6F 20 57 6F 72 6C 64 21
00000080 0A 00 00 00
B8 04 00 00 00 BB 01 00 00 00 B9 MM
00000090 MM MM MM BA 0C 00 00 00 CD 80 B8 01 00 00 00 BB
000000A0 00 00 00 00 CD 80 00 00
需要修改的部分已经标记,可参考结构解释填写。
HH HH HH HH: 程序入口地址
II II II II 指令在文件中长度,第二个为内存中的长度,二者相等。
JJ JJ JJ JJ 数据在文件中的偏移。
KK: 加载地址,应该说连这4个字节都算在内,不过需要写上很长的数据
和代码,
LL LL LL LL:数据在文件和内存中的长度,
MM MM MM MM:这个地址是我用的字符串在加载到内存后的地址,很容易通过
程序头的首地址确定。
这个简单的程序什么也做不了,我一直希望能够写机器码,以后有了这个相信容易多了。
而且这个程序,比用汇编编写的还要小很多。
后记:
正如我在论坛中所说的,代码还可以简单到只有一个代码段,而把数据嵌套到代码段里面。
不管怎么说,这个争论到此算是结束了。
参考资料:《Executable and Linkable Format (ELF)》(还有网上中文版的翻译)
《Intel® 64 and IA-32 Architectures Software Developer’s Manual》
而不是像我这样自己打文件头。可是被人耻笑说我脸计算机原理都不懂,我真的很伤心。
我一直信奉:The limit, forever is remains for these ignorant people。
(限制,永远是留给那些无知的人的。)
我承认他懂的比我多,但是他不该这么说我,这里我还是要重申,我知道linux不是windows,
但是没有DEBUG,我还是能写出来,就是麻烦点。
想编写可执行程序,就得了解ELF。
在研究下ELF文件头结构之前先看下32位平台的几个数据类型:
/* 32-bit ELF base types. */
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
#define EI_NIDENT 16
typedef struct elf32_hdr{
/** 魔数和一些平台版本信息。 前四个字节必须是0x454c46,
[4]表示平台位数, 1表示32位, 2表示64位。
[5]表示数据编码格式,1 表示小尾,2表示大尾。
[6]指定ELF头部版本,目前使用1,
这三位的完整取值可见/usr/include/linux/elf.h
中的定义。
[7]-[16]为填充,为0。
**/
unsigned char e_ident[EI_NIDENT]; /**魔数相关信息 **/
Elf32_Half e_type; /** 文件类型 **/
Elf32_Half e_machine; /** 硬件平台 **/
Elf32_Word e_version; /** 文件版本 **/
Elf32_Addr e_entry; /** 程序入口点 **/
Elf32_Off e_phoff; /** 程序头表偏移量 **/
Elf32_Off e_shoff; /** 节头表偏移量 **/
Elf32_Word e_flags; /** 处理器特定标志 **/
Elf32_Half e_ehsize; /** ELF头长度,即此结构长度, 52 **/
Elf32_Half e_phentsize; /** 程序头表中一个条目长度 **/
Elf32_Half e_phnum; /** 程序头表条目个数 **/
Elf32_Half e_shentsize; /** 节头表中一个条目长度 **/
Elf32_Half e_shnum; /** 节头表条目个数 **/
Elf32_Half e_shstrndx; /** 节头表字符串表索引 **/
} Elf32_Ehdr;
可以通过readelf -h 程序明查看可执行程序的ELF头,下面是个典型输出:
[souldump@localhost bin]$ readelf -h cpuid
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: Intel 80386
Version: 0x1
Entry point address: 0x8048074
Start of program headers: 52 (bytes into file)
Start of section headers: 252 (bytes into file)
Flags: 0x0
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: 6
Section header string table index: 3
ELF头结构之后跟随的是个程序头数组,从程序执行的角度分析,ELF文件被分为很多的段。
分别保存数据,指令,已初始化的数据。
typedef struct elf32_phdr{
Elf32_Word p_type; /** 段类型 **/
Elf32_Off p_offset; /** 段位置相对于文件开始的位移 **/
Elf32_Addr p_vaddr; /** 段在虚拟内存中的地址 **/
Elf32_Addr p_paddr; /** 段在物理内存中的地址 **/
Elf32_Word p_filesz; /** 段在文件中长度 **/
Elf32_Word p_memsz; /** 段在内存中长度 **/
Elf32_Word p_flags; /** 段标志 **/
Elf32_Word p_align; /** 段在内存中对齐标志 **/
} Elf32_Phdr;
查看cpuid程序的输出:
[souldump@localhost bin]$ readelf -l cpuid
Elf file type is EXEC (Executable file)
Entry point 0x8048074
There are 2 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x000ab 0x000ab R E 0x1000
LOAD 0x0000ac 0x080490ac 0x080490ac 0x00029 0x00029 RW 0x1000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
由于这是个由汇编语言编写的代码,没有调用GLIBC的库函数,因此它不需要解释器,不用加载
/lib/ld-linux.so.2.
下面是一个由C编译的程序的部分输出,如果想了解更详细的内容可以自己查看:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00464 0x00464 R E 0x1000
LOAD 0x000464 0x08049464 0x08049464 0x00100 0x00104 RW 0x1000
DYNAMIC 0x000478 0x08049478 0x08049478 0x000c8 0x000c8 RW 0x4
NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
程序头后面跟随的是节头数组:
typedef struct {
Elf32_Word sh_name; /** 小节名,在字符串表中索引 **/
Elf32_Word sh_type; /** 小节类型 **/
Elf32_Word sh_flags; /** 小节属性 **/
Elf32_Addr sh_addr; /** 小节在运行中的虚拟地址 **/
Elf32_Off sh_offset; /** 小节的文件偏移 **/
Elf32_Word sh_size; /** 小节的大小,字节为单位 **/
Elf32_Word sh_link; /** 链接另外一小节的索引 **/
Elf32_Word sh_info; /** 附加的小节信息 **/
Elf32_Word sh_addralign; /** 小节的对齐 **/
/** 如果一个小节保存着一张入定大小入口的表,就是入口的大小 否则为0 **/
Elf32_Word sh_entsize;
} Elf32_Shdr;
通常的C程序如果没使用其他共享库,会有27个section header.如果使用了其他的共享库,
则会按每个共享库一个条目(连接器ld-linux.so.2不算)。
比如/usr/bin/lsattr,由于使用了
0: libe2p.so.2 2007-03-22T05:13:36 0xda8e15c5 0 0
1: libcom_err.so.2 2007-03-22T05:12:10 0x805a324f 0 0
2: libc.so.6 2007-03-22T05:12:01 0x9e4b904b 0 0
3: /lib/ld-linux.so.2 2007-03-22T05:12:01 0x86c26626 0 0
因此会增加3个条目。
根据我对汇编语言编写的可执行代码分析,于C的方式很有很大区别。
使用汇编编译的程序,data节的数据,字符串末尾并不会添加"\0",输出到哪要靠用户自己控制,
而C编译的程序则会在字符串间添加.用C的程序data节的数据是按4字节对齐,但是,汇编中字符
串和数据是挨着排放的,后面直接安放了shstrtab节的数据。程序中定义的变量在函数入口开始
之前排放,就是在.text段开始处分配,而函数的入口则在占用的空间之后。比如.text段加载地址
000000,而我们定义了两个int型的变量,那么函数的实际入口应该为000008,(32位平台int为4字节
同样,如果增加一个字符串,那么也会增加一个指针的偏移(相关节,头的偏移和值也得修改)
你可以很简单的通过readelf程序的输出值比较得到结果。
从程序执行的角度来看,section头不是必须的,而从编译角度来看,程序头也不是必须的。
为了简单起见。我就没要section头,而且为了扩展方便,修改了代码段与数据段的位置。
这是我写出来的Hello World!程序,(虽然确很简单,但还是能说明问题):
用hexedit 16进制文本编辑器,编辑的。
最核心的应该算是那几条指令编码:
eax 的寄存器选择位为0, ebx的为3, 根据mov指令中的:
B8+ rd MOV r32, imm32 Move imm32 to r32.
以下两句:
movl $4, %eax
movl $1, %ebx
汇编为:
B8 04 00 00 00
BB 01 00 00 00
ECX的寄存器选择位为1.edx为2,第一句使用立即数寻址,虽然这是个变量,但是他存储的是地址。
从程序执行的角度来看,他存储的是字符串的的基地址。
所以下面两句:
movl $output, %ecx //随便取的名字。
movl $13, %edx
汇编为:
B9 XX XX XX XX (这里是字符串的首地址)
BA 0D 00 00 00
这是int指令的描述:
CD ib INT imm8 Interrupt vector number specified by immediate byte.
这句
int $0x80
被汇编为:
CD 80 (这里是一个字节啊,不是4个!)
movl $1, %eax
movl $0, %ebx
int $0x80
很容易写出还是上面的:
B8 01 00 00 00
BB 00 00 00 00
CD 80 00 00 //最后这俩是填充的。
/现在,这个.text节应该完成了:
当然了,可以直接写汇编的,然后编译后,然后在用hexedit编辑,把
指令拷贝出来,然后再修改地址。
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
02 00 03 00 01 00 00 00 84 80 04 08 34 00 00 00
74 00 00 00 00 00 00 00 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 80 04 08
00 80 04 08 A5 00 00 00 A5 00 00 00 05 00 00 00
00 01 00 00 01 00 00 00 74 00 00 00 74 90 04 08
74 90 04 08 0D 00 00 00 0D 00 00 00 06 00 00 00
00 01 00 00 48 65 6C 6C 6F 20 57 6F 72 6C 64 21
0A 00 00 00 B8 04 00 00 00 BB 01 00 00 00 B9 74
90 04 08 BA 0C 00 00 00 CD 80 B8 01 00 00 00 BB
00 00 00 00 CD 80 00 00
这个是我从上面的代码中分离的模板,认为可以做个程序通用部分,(代码可数据部分自己去写)
注:前面的是文件偏移量。
00000000 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
00000010 02 00 03 00 01 00 00 00 HH HH HH HH 34 00 00 00
00000020 74 00 00 00 00 00 00 00 34 00 20 00 02 00 00 00
00000030 00 00 00 00
01 00 00 00 00 00 00 00 00 80 04 08
00000040 00 80 04 08 II II II II II II II II 05 00 00 00
00000050 00 01 00 00
01 00 00 00 JJ JJ JJ JJ KK 90 04 08
00000060 KK 90 04 08 LL LL LL LL LL LL LL LL 06 00 00 00
00000070 00 01 00 00
48 65 6C 6C 6F 20 57 6F 72 6C 64 21
00000080 0A 00 00 00
B8 04 00 00 00 BB 01 00 00 00 B9 MM
00000090 MM MM MM BA 0C 00 00 00 CD 80 B8 01 00 00 00 BB
000000A0 00 00 00 00 CD 80 00 00
需要修改的部分已经标记,可参考结构解释填写。
HH HH HH HH: 程序入口地址
II II II II 指令在文件中长度,第二个为内存中的长度,二者相等。
JJ JJ JJ JJ 数据在文件中的偏移。
KK: 加载地址,应该说连这4个字节都算在内,不过需要写上很长的数据
和代码,
LL LL LL LL:数据在文件和内存中的长度,
MM MM MM MM:这个地址是我用的字符串在加载到内存后的地址,很容易通过
程序头的首地址确定。
这个简单的程序什么也做不了,我一直希望能够写机器码,以后有了这个相信容易多了。
而且这个程序,比用汇编编写的还要小很多。
后记:
正如我在论坛中所说的,代码还可以简单到只有一个代码段,而把数据嵌套到代码段里面。
不管怎么说,这个争论到此算是结束了。
参考资料:《Executable and Linkable Format (ELF)》(还有网上中文版的翻译)
《Intel® 64 and IA-32 Architectures Software Developer’s Manual》
相关阅读 更多 +