概述
使用C编译器制作简单二进制文件(i386+)
Cornelis Frank 著
newrain 译
2000.4.10
我写这篇文章是因为网上相关这方面的讨论不多,而我因为EduOS工程需要这么一篇文章。
由于使用下面的信息或程序引起的偶然的或一系列的致命错误,我将对此不负任何责任。
因此如果由于我的“英语”很差而使你的计算机因此遭受损坏,那是不是我的问题而是你的问题。
1 需要些什么工具?
·一个i386或跟高的PC机器
·像Red Hat或Slackware的一个Linux发行版操作系统
·GNU GCC 编译器。这个C编译器常被用于Linux系统中。使用下面的命令方式检测你的GCC版本:
gcc –version
将出现与下面类似的显示:
2.7.2 .3
你的计算机上显示的数值可能与上面的不匹配,但这个不是问题。
·Linux中的binutils工具
·NASM0.97或更高的版本。NASM最新广泛使用的版本是一个80x86汇编器,被设计成便携式和模块式。NASM支持大范围的对象文件格式,包括Linux的‘a.out’和ELF,NetBSD/FreeBSD,COFF,Microsoft 16-bit OBJ 和Win32 格式。它也能够输出简单的二进制文件。NASM的语法结构简单,容易理解,类似Intel的语法,但是跟简单。NASM支持Pentium,P6和MMX 操作代码和宏方法。
一般情况下,你可能没有NASM,从下面的地址下载:
http://sunsite.unc.edu/pub/Linux/devel/lang/assemblers/
·一个文本编辑框,如plco或emacs
1.1 安装广泛使用的汇编器
假定nasm-0.97.tar.gz在当前的目录下,键入:
gunzip nasm-0.97.tar.gz
tar –vxf nasm-0.97.tar
将会创建一个nasm-0.97的文件夹,进入这个文件夹,之后通过输入编译这个汇编器:
./configure
make
这个将创建执行文件nasm和ndisasm文件。可以将他们拷贝到/usr/bin目录下,这样可以方便访问他们。现在可以把nasm-0.97文件夹从系统中删除。我在Red Hat5.1和SlackWare3.1系统中成功编译了NASM,因此这个将不会是个大问题。
2 用C创建第一个简单的二进制文件
使用文本编辑器创建一个test.c文件,输入:
int main(){
}
输入编译:
gcc –c test.c
ld –o test –Ttext 0x0 –e main test.o
objcopy –R .note –R .comment –S –o binary test test.bin
这将创建一个二进制文件。我门可以使用ndisasm查看这个二进制文件,输入:
ndisasm –b 32 test.bin
将给出如下输出:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 C 9 leave
00000004 C 3 ret
将得到三列,第一列包括指令的内存地址,第二列包括指令的字节代码,最后一列为指令本身。
2.1 解析test.bin
刚才得到的代码似乎是设置了一个函数的基本框架。寄存器ebp将保存函数参数的句柄,以备使用。你将发现代码是32位的。GNU GCC也只能创建32位的代码。因此如果你向运行这些代码,你首先要设置一个向Linux一样的32位环境。那儿将会进入一个保护模式代码。你可以使用ld直间创建一个二进制文件。此处编译test.c如下:
gcc –c test.c
ld test.o –o test.bin –Ttext 0x0 –e main –oformat binary
这个将如先前的方法一样生成一个一模一样的二进制代码。
3 使用局部变量编程
下面我们将看GCC如何控制一个局部变量。此处我们创建一个新的test.c文件,其包含:
int main() {
int i; /* declaration of an int */
i = 0x12345678; /* hexdecimal */
}
输入如下编译:
gcc –c test.c
ld –o test –Ttext 0x0 –e main test.o
objcopy –R .note –R .coment –S –O binary test test.bin
编译之后我们将得到如下的二进制文件:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC04 sub esp,byte +0x4
00000006 C 745FC78563412 mov dword [ebp-0x4],0x12345678
0000000D C9 leave
0000000E C3 ret
3.1 解析test.bin
最前面两条指令和最后两条指令与前一个例子一样。这里面只有两条新的指令夹在旧指令之间。第一条指令esp减去4,这是GCC保留一个int的方法,在堆栈中有4个字节大小。下一条指令立即向我们示范了ebp寄存器的使用方法。寄存器在函数里保留不改变,在堆栈中仅涉及到局部变量的使用。堆栈中存放局部变量的地方常被叫做局部堆栈结构。在这篇文章中,ebp寄存器被叫做局部变量结构指针。
下一条指令用0x12345678填充了保留在堆栈上的int变量。也注意了保留顺序,过程存储数据的顺序。第二列,第四行,我们看到…78563412.这中现象叫做backwards storage1.
注意你也可以使用如下显示的ld命令创建一个直间的二进制文件,编译方式如下:
gcc –c test.c
ld –o test.bin –Ttext 0x0 –e main –oformat binary test.o
这将给出先前一样的二进制文件。
3.2 立即分配
我们将
int i;
i = 0x12345678;
改变为:
int i = 0x12345678 ;
我们将获得与先前一致的二进制文件。需要注意的是到目前为止我们还没有使用全局变量。
4 使用全局变量编程
下面我们将看看如何使用GCC编写一个全局变量。这将被使用在下一个test.c程序中
int i; /* declaration of global variable */
int main() {
i = 0x12345678 ;
}
输入如下编译:
gcc –c test.c
ld –o test –Ttext 0x0 –e main test.o
objcopy –R .note –R .comment –S –O binary test test.bin
这将使我们得到如下的二进制代码:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 C 7051010000078563412 mov dword [0x1010],0x12345678
0000000D C9 leave
0000000E C3 ret
4.1 解析test.bin
代码中间的指令将在内存中的某处写下我们的值,在我们的例子中这个地址是0x1010。这是由于连接器ld默认页属性的数据段地址。我们可以在连接器ld中使用参数-N来关闭这个性能。这将给我们如下代码:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 C 7051000000078563412 mov dword [0x10],0x12345678
0000000D C9 leave
0000000E C3 ret
我们现在发现,数据被存储在代码后面。我们也可以指定特殊的数据段。编译test.c程序:
gcc -c test.c
ld -o test -Ttext 0x0 -Tdata 0x1234 -e main -N test.o
objcopy -R .note -R .comment -S -O binary test test.bin
将给出如下二进制文件:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 C 7053412000078563412 mov dword [0x1234],0x12345678
0000000D C9 leave
0000000E C3 ret
现在全局变量被存储在0x1234地址处。因此如果在ld中使用参数-Tdata,我们可以指定局部的特殊数据段。另一方面,数据段在代码后被加载。通过在内存汇中存储变量,他将保留在main函数之外仍旧可以访问。这就是把int i叫做全局变量的原因。我们也用参数-oformat 在ld连接器中创建了二进制文件。
4.2 立即分配
一些我的实验经验告诉我,在二进制文件中立即分配全局变量可以被普通的变量控制,或可以像数据直接在代码后面存储,当数据常量被使用时,ld控制全局变量作为数据使用。
仔细看如下的程序:
const int c = 0x12345678;
int main() {
}
编译:
gcc –c test.c
ld –o test.bin –Ttext 0x0 –e main –N –oformat binary test.o
将给出如下的二进制文件:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 C 9 leave
00000004 C 3 ret
00000005 0000 add [eax],al
00000007 007856 add [eax+0x56],bh
0000000A 3412 xor al,0x12
可以看到在二进制文件后面有几个额外的字节。这个是一个只读数据节的包含全局变量的4个字节的属性。
4.2.1 objdump的使用
使用objdump可以得到更多的信息
objdump –disassemble –all test.o
将给我们下面的屏幕显示:
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 pushl %ebp
1: 89 e5 movl %esp,%ebp
3: c9 leave
4: c3 ret
Disassembly of section .data:
Disassembly of section .rodata:
00000000 <c>:
0: 78 56 js 58 <main+0x58>
2: 34 12 xorb $0x12,%al
我们将清楚地看到只读数据接包含全局变量常数c。仔细看下一个程序:
int i = 0x12345678;
const int c = 0x12345678;
int main () {
}
当我们编译这个程序,并使用objdump后可以得到:
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 pushl %ebp
1: 89 e5 movl %esp,%ebp
3: c9 leave
4: c3 ret
Disassembly of section .data:
00000000 <i>:
0: 78 56 js 58 <main+0x58>
2: 34 12 xorb $0x12,%al
Disassembly of section .rodata:
00000000 <c>:
0: 78 56 js 58 <main+0x58>
2: 34 12 xorb $0x12,%al
我们看到int i 在数据节中而常量c在只读数据节中。因此当ld使用全局变量常量,它将自动的使用数据节保存全局变量。
5 指针
现在我们先看看GCC如何终止一个指针变量,因此我们将先看下面的程序:
int main () {
int i;
int *p; /* a pointer to an integer */
p = &i; /* let pointer p points to integer i */
*p = 0x12345678; /* makes i = 0x12345678 */
}
这个程序将获得的二进制代码如下:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 8D55FC lea edx,[ebp-0x4]
00000009 8955F 8 mov [ebp-0x8],edx
0000000C 8B 45F 8 mov eax,[ebp-0x8]
0000000F C70078563412 mov dword [eax],0x12345678
00000015 C 9 leave
00000016 C 3 ret
5.1 解析test.bin
我们再次发现前两条和最后两条指令与前面一致。接下来将获取这样一条指令:
sub esp,byte+0x8
在堆栈中为局部变量保存了8个字节。指针的存储似乎占用4个字节。图1指示了当时的堆栈状况。你将看到lea指令将被加载到有效地址int i地址。下面这个值将被存储到int *p之中。之后int *p的值可以当作指针指向一个存有0x12345678值的双字节内存单元。
6 函数调用
现在我们看一下GCC如何引导函数调用。看下面的例子:
void f(); /* function prototype*/
int main() {
f(); /*function call*/
}
void f() { /* function definition */
}
将给出如下二进制代码:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 E804000000 call 0xc
00000008 C 9 leave
00000009 C 3 ret
0000000A 89F 6 mov esi,esi
0000000C 55 push ebp
0000000D 89E5 mov ebp,esp
0000000F C9 leave
00000010 C 3 ret
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 E804000000 call 0xc
00000008 C 9 leave
00000009 C 3 ret
0000000A 89F 6 mov esi,esi
0000000C 55 push ebp
0000000D 89E5 mov ebp,esp
0000000F C9 leave
00000010 C 3 ret
6.1 解析test.bin
在函数main中我们在0xC地址处调用了空函数f。这个空函数与main函数拥有同样结构。这也意味着在入口函数与其他函数之间没有其他结构差别。当你使用ld链接时,你加入ld的参数–M >mem.txt,将得到一个如何链接并存储的非常有用的文本文件。在文件mem.txt中,将在某个地方找到类似这两行:
Address of section .text set to 0x0
Address of section .data set to 0x1234
这表示二进制代码在地址0x0处喀什,全局变量存储的数据区在0x1234地址处。也可能发现类似的信息:
text 0x00000000 0x11
*(.text)
.text 0x00000000 0x11 test.o
0x 0000000c f
0x00000000 main
第一列包含节的名字,在这个例子为a.text节。第二列包含节的原始地址。第三表示为节的长度,最后一列为一些额外的信息,如函数名称,目标文件的使用。函数在0xC处开始偏移,函数是而今治文件的入口点。从最后一条指令(ret)起始地址为0x11,并占用1个字节,因此程序的长度为0x11。
6.2 objdump的使用
使用objdump可以显示目标文件的信息。这些信息对于查看目标文件的内部结构是非常有用的。使用objdump:
objdump –disassemble –all test.o
将在屏幕上输出:
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 pushl %ebp
1: 89 e5 movl %esp,%ebp
3: e8 04 00 00 00 call c <f>
8: c9 leave
9: c3 ret
a: 89 f 6 movl %esi,%esi
0000000c <f>:
c: 55 pushl %ebp
d: 89 e5 movl %esp,%ebp
f: c9 leave
10: c3 ret
Disassembly of section .data:
当我们学习GCC创建的二进制代码时,这是非常有用的。注意这儿并没有使用Intel语法显示指令。这儿使用pushl和movl等指令表示方式。指令结束处的l表示指令运行在32位操作数的平台中。另一个与Intel语法相反的重要语法是操作数倒置。下面的例子显示了两种不同语法下数据从寄存器EBX移动到寄存器EAX:
MOV EAX,EBX ;Intel syntax
movl %ebx, %eax ; ‘GNU’ syntax
Intel语法是第一个操作数为目标操作数,第二个为源操作数。
7 返回代码
你可能注意到我一直使用int main()作为函数定义,但是从来没有实际返回一个int。好的,我们试一下。
int main () {
return 0x12345678;
}
这个程序将给出如下代码:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 B878563412 mov eax,0x12345678
00000008 EB02 jmp short 0xc
0000000A 89F 6 mov esi,esi
0000000C C9 leave
0000000D C3 ret
7.1 解析test.bin
可以看到值通过寄存器eax返回,因为我不需要显式的填充返回值到寄存器。因此我们也不要返回。此外还有一个优点,返回值存储在寄存器汇总,我们不需要显式的读取返回值。我们可以一直使用ANSI C函数printf在屏幕上打印一些信息:
printf(…);
printf给调用者返回一个整数。当然,如果返回的参数类型大于4个字节,编译器不会使用这种方法。下一章将解释此种情况的发生。
7.2 返回数据结构
考虑下面的程序:
typedef struc{
int a,b,c,d;
int i[10];
}MyDef;
MyDef MyFunc(); /* function prototype */
int main(){ /* entry point*/
MyDef d;
d = MyFunc();
}
MyDef MyFunc() { /* a local function */
MyDef d;
return d;
}
这个程序将得到下面的二进制代码:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC38 sub esp,byte +0x38
00000006 8D 45C 8 lea eax,[ebp-0x38]
00000009 50 push eax
0000000A E805000000 call 0x14
0000000F 83C 404 add esp,byte +0x4
00000012 C 9 leave
00000013 C 3 ret
00000014 55 push ebp
00000015 89E5 mov ebp,esp
00000017 83EC38 sub esp,byte +0x38
0000001A 57 push edi
0000001B 56 push esi
0000001C 8B4508 mov eax,[ebp+0x8]
0000001F 89C 7 mov edi,eax
00000021 8D 75C 8 lea esi,[ebp-0x38]
00000024 FC cld
00000025 B90E000000 mov ecx,0xe
0000002A F 3A 5 rep movsd
0000002C EB02 jmp short 0x30
0000002E 89F 6 mov esi,esi
00000030 89C 0 mov eax,eax
00000032 8D 65C 0 lea esp,[ebp-0x40]
00000035 5E pop esi
00000036 5F pop edi
00000037 C 9 leave
00000038 C 3 ret
解析test.bin
函数main中的地址0x3,编译器在堆栈中保留了38个字节。这个是结构MyDef的大小,地址0x6到0x9,我们看到解决的方法了,当MyDef大于4字节,编译器通过一个指针指向地址0x14函数MyFunc中的d。注意参数传递到函数Myfunc,事实上C函数中没用定义任何参数。为了填充数据结构MyFunc使用了32位数据移动指令:
0000002A F 3A 5 rep movsd
7.3 返回数据结构Ⅱ
当然,我们可以向自己提这样一个问题:当我们不想存储返回的数据结构,哪一个指针将给予函数MyFunc。因此,考虑下面的程序:
typedef struct {
int a,b,c,d;
int i[10];
}MyDef;
MyDef MyFunc(); /* function prototype */
int main() { /* entry point */
MyFunc() ;
}
MyDef MyFunc() { /* a local function */
MyDef d;
return d;
}
获得的二进制代码:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC38 sub esp,byte +0x38
00000006 8D 45C 8 lea eax,[ebp-0x38]
00000009 50 push eax
0000000A E805000000 call 0x14
0000000F 83C 404 add esp,byte +0x4
00000012 C 9 leave
00000013 C 3 ret
00000014 55 push ebp
00000015 89E5 mov ebp,esp
00000017 83EC38 sub esp,byte +0x38
0000001A 57 push edi
0000001B 56 push esi
0000001C 8B4508 mov eax,[ebp+0x8]
0000001F 89C 7 mov edi,eax
00000021 8D 75C 8 lea esi,[ebp-0x38]
00000024 FC cld
00000025 B90E000000 mov ecx,0xe
0000002A F 3A 5 rep movsd
0000002C EB02 jmp short 0x30
0000002E 89F 6 mov esi,esi
00000030 89C 0 mov eax,eax
00000032 8D 65C 0 lea esp,[ebp-0x40]
00000035 5E pop esi
00000036 5F pop edi
00000037 C 9 leave
00000038 C 3 ret
解析
这些代码显示了,在地址0x0的main函数入口处尽管没有任何局部变量,但是函数在堆栈的某个地方精确保存了0x38大小的变量。此后,一个指向这个数据结构的指针在地址0x14处被传送到函数MyFunc中,如先前的例子。当然注意到函数MyFunc内部没有改变数据结构。
8 传递函数参数
这一节,我们将仔细看一下如何将函数参数传递到函数中。看下面的例子:
char res; /* global variable */
char f (char a, char b); /* function prototype */
int main() { /* entry point */
res = f (0x12,0x23); /* function call */
}
char f (char a, char b) { /* function definition */
return a+b; /*return code */
}
将获得如下的二进制代码:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 6A 23 push byte +0x23 /* 参数入栈*/
00000005 6A 12 push byte +0x12
00000007 E810000000 call 0x 1c /* 函数调用*/
0000000C 83C 408 add esp,byte +0x8
0000000F 88C 0 mov al,al
00000011 880534120000 mov [0x1234],al
00000017 C 9 leave
00000018 C 3 ret
00000019 8D7600 lea esi,[esi+0x0]
0000001C 55 push ebp
0000001D 89E5 mov ebp,esp
0000001F 83EC04 sub esp,byte +0x4
00000022 53 push ebx
00000023 8B5508 mov edx,[ebp+0x8]
00000026 8B4D 0C mov ecx,[ebp+0xc]
00000029 8855FF mov [ebp-0x1],dl
0000002C 884DFE mov [ebp-0x2],cl
0000002F 8A 45FF mov al,[ebp-0x1]
00000032 0245FE add al,[ebp-0x2]
00000035 0FBED8 movsx ebx,al
00000038 89D8 mov eax,ebx
0000003A EB00 jmp short 0x 3c
0000003C 8B5DF8 mov ebx,[ebp-0x8]
0000003F C9 leave
00000040 C 3 ret
8.1 C 语言调用惯例
我们注意到第一件事情是将参数倒序压入堆栈,这个就是C语言调用惯例。下面给出了32-位程序C语言调用的惯例。在下面的描述中,字caller(调用函数)和callee(被调用函数)用于区别调用函数和被调用函数。
调用函数将函数参数一个接一个倒序压入堆栈(从右到左,因此第一个参数被函数最后压入堆栈)。
调用函数将执行一个近的CALL指令转到被调用函数处。
被调用函数获得控制,一般而言(虽然在函数内部不是必需要使用这些参数,)函数始于在EBP中保存ESP的值,使得EBP作为堆栈中寻找参数的基准指针。然而,调用函数也有可能已经完成了这件事,所以调用惯例描述EBP必需在任何C函数中被保留。因此,如果被调用函数设置EBP作为一个框架指针,必需先保存EBP的值。
被调用函数可能会通过相对EBP相对指针使用参数。双字节[EBP]保留了先前压入堆栈的EBP的值;下一个双字节[EBP+4]保留了CALL明显压入的返回地址。参数保留在[EBP+8]的起始地址中。函数最左边的参数,由于最后被压入堆栈,在EBP+8偏移量处进入。其他跟随的是一些更大的偏移量。因此,一个函数如printf可以使用可变数目的参数,参数倒序压入意味着函数能够在哪儿找到第一参数,这恰恰表明了函数参数的个数和保留的类型。
被调用函数也可以使减去更大值的ESP为局部变量获取堆栈空间,这将使EBP使用负偏移获取参数。
如果调用函数需要一个返回值,被调用函数应该将值保留在AL,AX或EAX中(依赖于值的字节数),一般浮点值在ST0中返回。
如果被调用函数结束进程并使用ESP获取了局部堆栈空间,将从EBP中恢复ESP的内容,然后弹出先前的EBP值,使用ret(等同retn)返回
当调用函数再次从被调用函数中获取控制权,函数参数仍旧在堆栈中,因此一般将一个立即数加给ESP以移交他们(取代执行执行速度慢的POP指针)。因此,一般情况下,如果一个函数由于类型匹配错误,被一个错误数目参数的函数调用,堆栈由于指导具体参数数目的调用函数的移交将返回一个敏感的内容。
8.2 解析
因此在两个字节被压入堆栈后,在0x 1c 处有一f函数的调用。这个函数首先因局部函数使esp减去了4,之后进行了函数参数的局部复制,然后a+b被计算,在寄存器eax中返回计算值。
9 32-位堆栈属性
注意——即使两个参数作为bytes被压入堆栈——函数在堆栈中如双字节获取参数!这表示在32-位模式下压入字节如同双字压入堆栈。这是因为堆栈属性是32-位2的。当你在汇编中不得不写32-位遵循C调用惯例的函数时,这是非常重要的。
10 其他语句
当然我们可以看GCC如何控制for循环,while循环,if-else语句和case结构,没关系,当你需要写这些的时候再看。如果你嫌麻烦,也没关系,你不一定要写这些东西。
11基础数据类型之间的惯例
在这一部分,我们将仔细看一下C编译如何转换基础数据类型,这些数据类型如下:
signed char 和unsigned char (1个字节)
signed short和unsigned short(2个字节)
signed int 和unsigned int (4个字节)
首先我们将看计算机怎么控制这些数据类型。
11.1 二进制补数
在Intel结构IA-32中使用二进制补数来表示一个符号整数。二进制补数的无符号的整数n表示为位串,通过2为底,n为指数获取整数。如果使用bitwise的位串补数形式,并加上1,将获取二进制补数-n的表示。整数在内存中使用二进制补数表示的机器叫做“二进制补数计算机”。注意,二进制补数0和-0都使用包含所有0的二进制字串表示。例如;
(0)10 = (0000 0000)2
(-0)10 = (0000 0000)2+1
= (1111 1111)2+1
= (0000 0000)2
= (0)10
此处(…)x表示一个数以x为基。注意,高位开表示负数。当然,你也不必自己转换一个特定基的负数。IA-32结构有一个特殊的指令用于这方面的,这个指令为NEG。表1向我们显示了一个字节表示的两种补数。
表1:一个字节的两种补数
使用两种补数表示方法使得计算负数的时候可以像使用正数一样计算。
11.2 分配
此处我们将看一些C的属性和它汇编后的结果。使用的C程序如下:
main() {
unsigned int i=251;
}
当我们编译成简单二进制文件,我们得到:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC04 sub esp,byte +0x4
00000006 C 745FCFB000000 mov dword [ebp-0x4],0xfb
0000000D C9 leave
0000000E C3 ret
当我们把常用的分配改为:
unsigned int i= -5;
我们将在0x6处得到下一个指令:
00000006 C 745FCFBFFFFFF mov dword [ebp-0x4],0xfffffffb
现在我们仔细看一下符号整数,语句为:
int i=251;
结果为:
C745FCFB000000 mov dword [ebp-0x4],0xfb
使用负数的语句:
int i=-5;
结果为:
00000006 C 745FCFBFFFFFF mov dword [ebp-0x4],0xfffffffb
符号和无符号的数据分配处理方法相同。
11.3 将符号的char转换为符号的int
此处我们将研究下面的小程序:
main() {
char c=-5;
int i;
i = c;
}
我们将获得二进制文件:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 C 645FFFB mov byte [ebp-0x1],0xfb
0000000A 0FBE45FF movsx eax,byte [ebp-0x1]
0000000E 8945F 8 mov [ebp-0x8],eax
00000011 C 9 leave
00000012 C 3 ret
解析
首先我们在地址0x3处为局部变量c和i在堆栈中转换为8个字节。编译器占用8个字节使得整数排列成为可能。下面,我们看到在地址[ebp-0x1]处字符c被0xfb填充,0xfb当然表示-5。(0xfb=251,251-256=-5)。注意编译器也使用了[ebp-0x1],而不是[ebp-0x4]。这是由于little endian表示方法。下一条指令movsx将一个符号型字符转数据化为符号型整数。MOVSX符号扩展它的源操作数(第二个)到目标操作数(第一个)的长度,并复制结果到目标操作数。最后一条指令(在leave之前)将存储在eax中符号整数写入整型i中。
11.4 转换符号整数到符号字符
看一下相反的转换
main() {
char c;
int i=-5;
c = i;
}
注意语句c=i仅在i的值在-128到127之间敏感。因为他的范围超过了signed char所能表示的范围。编译获得二进制文件:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 C 745F 8FBFFFFFF mov dword [ebp-0x8],0xfffffffb
0000000D 8A 45F 8 mov al,[ebp-0x8]
00000010 8845FF mov [ebp-0x1],al
00000013 C 9 leave
00000014 C 3 ret
解析
0xfffffffb 就是-5。当我们仅看最低符号字节0xfb,我们移动这个数据到singed char中,我们也将得到-5。因此,一个signed int转换到一个singed char,我们使用了一个简单的mov指令。
11.5 转换unsined char为unsigned int
看下面的C程序
main() {
unsigned char c=5;
unsigned int i;
i = c;
}
我们将获取二进制文件:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 C 645FF05 mov byte [ebp-0x1],0x5
0000000A 0FB645FF movzx eax,byte [ebp-0x1]
0000000E 8945F 8 mov [ebp-0x8],eax
00000011 C 9 leave
00000012 C 3 ret
解析
除了在地址0xA处的指令不一致之外,将signed char转换为signed int,我们将得到一样的二进制文件。此处,我们使用的是movzx指令。MOVZX零扩展它的源操作数(第二个)到目标操作数(第一个)的长度,并复制结果到目标操作数。
11.6 转换unsigned int到unsigned char
此处我们使用程序
main() {
unsigned char c;
unsigned int i=251;
c=i;
}
再次注意整数值限制在0-255之间。这是因为一个unsigned char无法控制更大的数值。相应获得的二进制为:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 C 745F 8FB000000 mov dword [ebp-0x8],0xfb
0000000D 8A 45F 8 mov al,[ebp-0x8]
00000010 8845FF mov [ebp-0x1],al
00000013 C 9 leave
00000014 C 3 ret
解析
事实上转换指令,在0xD地址处mov指令,对于转换符号整数到符号字符是一样的。
11.7 转换signed int 到unsigned int
程序入下:
main() {
int i=-5;
unsigned int u;
u=i;
}
二进制文件:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 C 745FCFBFFFFFF mov dword [ebp-0x4],0xfffffffb
0000000D 8B45FC mov eax,[ebp-0x4]
00000010 8945F 8 mov [ebp-0x8],eax
00000013 C 9 leave
00000014 C 3 ret
解析
在符号和无符号整数之间的转换没有特殊之处。惟一的差别是当使用整数操作时,符号整数将使用如idiv,imul等指令,而无符号整数使用div,mul等无符号版本的指令。
12 GCC编译的代码的基本环境
因为我没有发现任何官方文件有关这方面的内容的说明,因此我自己将试图将这些找出来。我得到的为:
32-位模式,因此在GDT或LDT表中开启32位代码标识的保护模式。
段寄存器CS,DS,ES,FS,GS和SS指向同一个内存区域。(别名)
由于没有初始化的全局变量存储在在代码后的的“right”中,因此你需要保留一小块自由空间。这块区域叫做BSS节。注意二进制文件初始化的全局变量存储在代码节后的DATA节中。常量申明的变量存储在RODATA(只读)节中,这一节也是二进制文件的部分。
确信堆栈不会覆盖代码和全局变量。
在Intel文件中[2],他们将这些叫做Basic Flat Model。不要误解这个。我们不必使用Basic Flat Model。只要C编译的二进制文件使CS,DS和SS指向同一块内存区域(使用别名),所有的代码都能很好运行。因此,只要每个C编译二进制文件使用局部basic flat内存模式我们充分利用多端保护页模式
13 外部可共享的全局变量
这一节,我们将了解如何从非C程序中访问全局C变量。当你用另一个程序(汇编编写)载入C程序时非常有用的,这个程序一般必需初始化C程序中的全局变量。当然,我们可以使用C程序的堆栈传递变量,但是这些变量将一直存储在堆栈汇中,而这不是我们的目的。我们也也可以在内存某处使用一个全局变量表作为一个定点,这样C程序进入这个地址如同一个常数,但是我们将使用愚蠢的指针指向那个表。下面是我们所做的,在文件test.c中:
int myVar=5;
int main() {
}
我们编译C程序:
gcc –c test.c
ld –Map memmap.txt –Ttext 0x0 –e main –oformat binary –N -0 test.bin test.o
ndisasm –b 32 test
将获取:
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 C 9 leave
00000004 C 3 ret
00000005 0000 add [eax],al
00000007 00 db 0x00
00000008 05 db 0x05
00000009 0000 add [eax],al
0000000B 00 db 0x00
你可以看到变量myVar存储在局部地址0x8中。现在我们必需从使用-Map参数的ld命令后的内存映像文件memmap.txt中获取地址。因此我们使用命令:
cat memmap.txt | grep myVar | grep –v ‘/.o | sed ‘s/ *//’ | cut –d’ ‘ –f1
这个在模块test.o中给我们变量myVar的地址。
0x00000008
当我们在环境变量(UNIX)MYVAR中输入,我们能够使用这个值传递给nasm寻找全局C变量myVar。例如:
nasm –f bin –d MYVAR_ADDR=$MYVAR –o init.bin init.asm
在init.asm代码中,使用方法如下:
…
mov ax,CProgramSelector
mov es,ax
mov eas,[TheValueThatMyVarShouldContain]
mov [es:MYVAR_ADDR],eax
…
13.1 BBS节的大小
当C程序作为一个可心,必需知道内存管理的BSS节有多大。这个尺寸也可以从memmap.txt中扩展出来。因此使用:
cat memmap.txt | grep ‘/.bss’ | grep –v ‘/.o’ | sed ‘s/.*0x/0x/’
对于我们的例子test.c,将获得
0x0
我们传递这个值如同们使用全局变量传递那样。
13.2 全局静态变量
在C中,没有在直接访问静态变量的方法。这是因为他们被申明为静态。这个规则也适用于描述外部进入方法。当一个全局变量被申明为静态,链接器ld产生的内存映像文件中没有申明的全局静态变量的地址。因此,我们无法确定全局静态变量的地址。关键字static提供了一种保护机制。
14 在IA-32中ANSI C stdarg.h的实现
这个头文件提供了程序员合适的写函数的方法,如printf可有多个参数。头文件包含一个typedef和三个宏。虽然这些实现是系统相关的,但是在IA-32中可能实现是:
#ifndef STDARG_H
#define STDARG_H
typedef char *va_list;
#define va_rounded_size(type) /
(((sizeof(type) + sizeof (int) -1) / sizeof(int)) *sizeof(int))
#define va_start(ap,v) /
( (void) (ap = (va_list) &v +va_rounded_size (v) ))
#define va_arg(ap,type) /
(ap += va_rounded_size (type),*((type*) (ap-va_rounded_size(type))))
#define va_end(ap) ((void) (ap=0))
#endif
在宏va_start中,变量v是头文件中可变参数函数定义中的最后参数变量。v变量不能够存储在寄存器类中,也不能够存储为数组类型或一个类似字符自动扩展的其他类型。红va_start以指针ap初始化。宏va_arg在参数列表中指向下一个参数。宏va_end将所有存储清空,这是在函数退出前必需拥有的。
在给出的实现方法中,我们适用了宏va_rounded_size。由于在32-位边界堆栈(用来传递一个函数的参数)的IA-32属性,va_rounded_size宏是必需的,使用语句sizeof(int)表示。宏va_start将是参数指针ap指向给出变量v之后的变量。va_start宏不能返回返回任何值(以(void)开头)
宏va_arg首先增加了给出类型大小的参数指针ap值。之后,将返回堆栈中类型大小的下一个指针(事实上前一个参数,参数指针首先增加)。起初看来,这种控制方法似乎非常不可思议,但是由于我们在最后一个逗号后,输入一个必需在宏定义的末尾返回的变量,这将是惟一的方法。
最后,宏va_end将重新设置参数指针ap,并且什么也不返回。
References
[1] A Book on C
Programming in C, fourth edition
Addison-Wesley— ISBN 0-201-18399-4
[2] Intel Architecture Software Developer’s Manual
Volume 1: Basic Architecture
Order Number: 243190
Volume 2: Instruction Set Reference Manual
Order Number: 243191
Volume 3: System Programming Guide
Order Number: 243192
[3] NASM documentation
http://www.cryogen.com/Nasm
[4] Manual Pages
gcc, ld, objcopy, objdump
最后
以上就是烂漫招牌为你收集整理的使用C编译器制作简单二进制文件(i386+)的全部内容,希望文章能够帮你解决使用C编译器制作简单二进制文件(i386+)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复