使用GCC生成无格式二进制文件(plain binary files)

使用GCC生成无格式二进制文件(plain binary files

我在互联网上搜索很久,只找到一些零星的关于这方面的资料。我想使用gcc开发一个自己使用的专用工具,结合自己的工作经验,写了这篇总结性的资料。

1.         软硬件环境

l          至少一台正在使用的80x86系列的32-bit电脑,越高档越好。

l          一套Linux的发行版,如RedhatMandrakeTurboLinux等。

l          GNU GCC编译器。该编译器在Linux下很常用。

l          Linux上的binutils

l          自己熟悉的文本编辑器,如vi等。

如果你不具备这些条件,就不要再往下看了。我的工作环境是,在一台赛扬433上安装了Redhat Linux8.0128M内存,gcc是默认的,版本为3.2.2。可以使用如下命令查看gcc的版本:

gcc --version

 

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

最后生成的二进制文件是test.bin,可以使用你喜欢的反汇编工具看看这个文件里到底是什么。我使用Linux下的objdump进行反汇编:

objdump –D –b binary –a i386 test.bin

结果如下:

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c9                        leave 

  11:          c3                        ret  

其中第一列是指令的内存地址;第二列是指令的机器码;第三列是汇编指令。相信你的结果与此同。如果你的gcc与我的不一样,例如2.7.x版本的gcc,你的结果很可能会有所不同,缺少如下的四条指令,这是正常的,这两个版本的gcc所使用的堆栈框架不同(下面介绍的例子也会因为编译器版本的不同造成其结果有别):

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp #堆栈对齐,以16Bytes为单位分配局部变量空间

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

上述代码都是32-bit代码,你需要在像Linux这样的 32-bit环境下运行,并且是保护模式。也可以只用下面的指令直接生成test.bin

gcc –c test.c

ld –Ttext 0x0 –e main --oformat binary –o test.bin test.o

上面的test.c中只有一个函数,而且还只是个框架。其反汇编代码也没什么难理解的。

3.         编写带局部变量的程序

再创建一个新的test.c,看看gcc是如何处理局部变量的。

int main()

{

int i;

i=0x12345678;

}

使用上述两种方法的人一种编译,生成test.bin。然后使用objdump进行反汇编:

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c7 45 fc 78 56 34 12    movl   $0x12345678,0xfffffffc(%ebp)

  17:          c9                        leave 

  18:          c3                        ret

与第一个例子相比,开头的六条指令和最后的两条指令完全相同,仅有一条指令不同。这条语句是给局部变量赋值,其空间的分配在前面已经进行了。在gcc中,堆栈中的局部变量空间按16字节为单位进行分配,而不是通常的1字节为单位。如果将

int i;

i=0x12345678;

改为

int i=0x12345678;

其结果没有区别。但是,如果是全局变量,就不一样了。

4.         编写带全局变量的程序

test.c改为:

int i;

int main()

{

i=0x12345678;

}

使用同样的方法编译,然后再进行反汇编:

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c7 05 1c 10 00 00 78   movl   $0x12345678,0x101c

  17:          56 34 12

  1a: c9                        leave 

  1b:          c3                        ret   

我们定义的全局变量被放到了0x101c处,这是gcc默认以page-align对齐数据段的结果,此处的page与页式内存管理中的page没有关系。在使用ld链接时,使用-N参数可以关闭对齐效果。

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c7 05 1c 00 00 00 78   movl   $0x12345678,0x1c

  17:          56 34 12

  1a: c9                        leave 

  1b:          c3                        ret 

正如我们看到的,数据段紧接着代码段。我们也可以明确的指定数据段的位置,试试下面的命令再进行编译:

gcc –c test.c

ld –Ttext 0x0 –Tdata 0x1234 –e main –N --oformat binary –o test.bin test.o

然后再使用objdump进行反汇编:

00000000 <.data>:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c7 05 34 12 00 00 78   movl   $0x12345678,0x1234

  17:          56 34 12

  1a: c9                        leave 

  1b:          c3                        ret   

现在,我们定义的全局变量被放到0x1234处了。通过给ld指定-Tdata参数,可以自由的定义数据段的地址,如果不指定,数据段在代码段后。

再看看直接给全局变量进行初始化的情况。

const int I=0x12345678;

int main()

{

}

仍然使用上面的方法进行编译、链接、反汇编,其结果如下:

00000000 <.data>:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c9                        leave 

  11:          c3                        ret   

  12:          00 00                      add    %al,(%eax)

  14:          78 56                      js     0x6c

  16:          34 12                      xor    $0x12,%al

代码以4Bytes对齐,全局变量被直接存储在代码段之后的数据段,ld直接将常数放到了全局变量的位置,一步到位。

使用如下命令可以看到更多细节:

objdump –D test.o

可以看到如下的结果:

test.o:     file format elf32-i386

 

Disassembly of section .text:

 

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c9                        leave 

  11:          c3                        ret   

Disassembly of section .data:

Disassembly of section .rodata:

 

00000000 :

   0:          78 56                      js     58

   2:          34 12                      xor    $0x12,%al

我们可以更清楚地看到,在.c文件中定义的全局常量被放在了只读的数据段中了。再看下面的一段代码:

int I=0x12345678;

const int c=0x12345678;

int main()

{

}

还是使用上面的方法编译、链接、反汇编,可以到到如下结果:

test.o:     file format elf32-i386

 

Disassembly of section .text:

 

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c9                        leave 

  11:          c3                        ret   

Disassembly of section .data:

 

00000000 :

   0:          78 56                      js     58

   2:          34 12                      xor    $0x12,%al

Disassembly of section .rodata:

 

00000000 :

   0:          78 56                      js     58

   2:          34 12                      xor    $0x12,%al

可以看出,整数I被放在了普通的数据段中,常数c被放在了只读数据段中了。当使用全局变量(常量)时,ld会自动的使用合适的数据段存储他们。

5.         处理指针

使用如下代码来查看gcc处理指针变量的情况:

int main()

{

int I;

int* p;

p=&I;

*p=0x12345678;

}

使用objdump查看生成的机器代码:

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:         8d 45 fc                     lea    0xfffffffc(%ebp),%eax

  13:         89 45 f8                    mov    %eax,0xfffffff8(%ebp)

  16:         8b 45 f8                    mov    0xfffffff8(%ebp),%eax

  19:         c7 00 78 56 34 12    movl   $0x12345678,(%eax)

  1f: c9                        leave 

  20:          c3                        ret   

一开始,gcc已经为局部变量预分配了至少8Bytes的空间,并且使esp16Bytes边界对齐,如果还需要额外的空间,gcc将按照16Bytes为单位进行分配,而不是其他编译器所使用的以1Byte为单位进行分配。变量I位于ebp-4,变量p位于ebp-8lea指令将I的有效地址放入eax中,然后又被放入p中。最后,将0x12345678赋给p指向的变量I

6.         关于函数调用

看如下代码:

void func();

int main()

{

func();

}

void func()

{

}

再看生成的二进制代码:

00000000 <.data>:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          e8 03 00 00 00                call   0x18

  15:          c9                        leave 

  16:          c3                        ret   

  17:          90                        nop   

  18:          55                        push   %ebp

  19:          89 e5                      mov    %esp,%ebp

  1b:          c9                        leave 

  1c: c3                        ret   

主函数main通过call指令调用了空函数func,该函数与main大同小异。为ld指定-Map开关输出map文件,可以得到更详细的信息。

.text           0x00000000       0x1d

 *(.text .stub .text.* .gnu.linkonce.t.*)

 .text          0x00000000       0x1d       test.o

                0x00000000                main

                0x00000018                func

第一列是段名,这里是.text;第二列是起始位置,第三列是段长度,最后一列是附加信息,如函数名、所出自的目标文件等。可以看到,.text段从0x0开始,长度为0x1d;函数func0x18开始。

7.         函数的返回值

看下面的代码,主函数main返回一个整型值:

int main()

{

return 0x12345678;

}

所生成的二进制代码与其他编译器大同小异:

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          b8 78 56 34 12                mov    $0x12345678,%eax

  15:          c9                        leave 

  16:          c3                        ret   

你已经看到了,gcc使用eax传递返回值。因为返回值就是eax寄存器的值,所以你可以隐含的返回,甚至什么都不返回。因为返回值保存在寄存器中,进行函数调用时,经常忽略返回值。例如,我们经常这样调用函数:

printf(…);

该函数是有返回值的。如果函数返回的数据大于4Bytes,就不能再使用这种方法返回数据了。再看下面的例子:

typedef struct mydef{

int a,b,c,d;

int array[10];

}mydef;

 

mydef func();

int main()

{

mydef d;

d=func();

}

mydef func()

{

mydef d;

return d;

}

接着看反汇编的代码:

00000000 <.data>:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 48                    sub    $0x48,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          8d 45 b8                    lea    0xffffffb8(%ebp),%eax

  13:          83 ec 0c                     sub    $0xc,%esp

  16:          50                        push   %eax

  17:          e8 06 00 00 00                call   0x22

  1c: 83 c4 0c                    add    $0xc,%esp

  1f: c9                        leave 

  20:          c3                        ret   

  21:          90                        nop   

  22:          55                        push   %ebp

  23:          89 e5                      mov    %esp,%ebp

  25:          57                        push   %edi

  26:          56                        push   %esi

  27:          83 ec 40                    sub    $0x40,%esp

  2a: 8b 7d 08                    mov    0x8(%ebp),%edi

  2d:          8d 75 b8                    lea    0xffffffb8(%ebp),%esi

  30:          fc                         cld   

  31:          b8 0e 00 00 00                mov    $0xe,%eax

  36:          89 c1                      mov    %eax,%ecx

  38:          f3 a5                       repz movsl %ds:(%esi),%es:(%edi)

  3a: 8b 45 08                    mov    0x8(%ebp),%eax

  3d:          83 c4 40                    add    $0x40,%esp

  40:          5e                        pop    %esi

  41:          5f                        pop    %edi

  42:          c9                        leave 

  43:          c2 04 00                    ret    $0x4

我们自定义的结构为0x38Bytesgcc为了保持堆栈的16Bytes对齐,分配了0x40Bytes的空间。函数func并没有参数,但是在调用时,却将变量d的指针传了进去。然后利用这个指针,使用指令movsl直接对d进行赋值。再看下面的例子:

typedef struct mydef{

int a,b,c,d;

int array[10];

}mydef;

 

mydef func();

int main()

{

func();

}

mydef func()

{

mydef d;

return d;

}

再看反汇编的结果:

00000000 <.data>:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 48                    sub    $0x48,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          8d 45 b8                    lea    0xffffffb8(%ebp),%eax

  13:          83 ec 0c                     sub    $0xc,%esp

  16:          50                        push   %eax

  17:          e8 06 00 00 00                call   0x22

  1c: 83 c4 0c                    add    $0xc,%esp

  1f: c9                        leave 

  20:          c3                        ret   

  21:          90                        nop   

  22:          55                        push   %ebp

  23:          89 e5                      mov    %esp,%ebp

  25:          57                        push   %edi

  26:          56                        push   %esi

  27:          83 ec 40                    sub    $0x40,%esp

  2a: 8b 7d 08                    mov    0x8(%ebp),%edi

  2d:          8d 75 b8                    lea    0xffffffb8(%ebp),%esi

  30:          fc                         cld   

  31:          b8 0e 00 00 00                mov    $0xe,%eax

  36:          89 c1                      mov    %eax,%ecx

  38:          f3 a5                       repz movsl %ds:(%esi),%es:(%edi)

  3a: 8b 45 08                    mov    0x8(%ebp),%eax

  3d:          83 c4 40                    add    $0x40,%esp

  40:          5e                        pop    %esi

  41:          5f                        pop    %edi

  42:          c9                        leave 

  43:          c2 04 00                    ret    $0x4

可以说,与上面的结果一字不差!我们没有在main函数中声明变量存储func返回的结果,但是gcc替我们做了。它仍然为函数func传递了一个指针,并将结果传了出来,尽管我们对返回值不感兴趣,但编译器对我们的兴趣好像也没有兴趣,依然我行我素。(如果使用了优化选项,结果很可能有所相同)。

8.         给函数传递参数

gcc遵循一般的c语言标准,包括参数传递方式。看看下面的例子:

char res;

char func(char a,char b);

int main()

{

res=func(0x02,0x03);

}

char func(char a,char b)

{

return a+b;

}

再看看他的反汇编代码:

00000000 <.data>:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          83 ec 08                    sub    $0x8,%esp

  13:          6a 03                      push   $0x3

  15:          6a 02                      push   $0x2

  17:          e8 0a 00 00 00                call   0x26

  1c: 83 c4 10                    add    $0x10,%esp

  1f: a2 44 00 00 00                mov    %al,0x44

  24:          c9                        leave 

  25:          c3                        ret   

  26:          55                        push   %ebp

  27:          89 e5                      mov    %esp,%ebp

  29:          83 ec 04                    sub    $0x4,%esp

  2c: 8b 45 08                    mov    0x8(%ebp),%eax

  2f: 8b 55 0c                    mov    0xc(%ebp),%edx

  32:          88 45 ff                     mov    %al,0xffffffff(%ebp)

  35:          88 55 fe                     mov    %dl,0xfffffffe(%ebp)

  38:          8a 45 fe                     mov    0xfffffffe(%ebp),%al

  3b:          02 45 ff                     add    0xffffffff(%ebp),%al

  3e: 0f be c0                     movsbl %al,%eax

  41:          c9                        leave 

  42:          c3                        ret   

如果你精通汇编语言,看完这段代码,恐怕你已经口吐鲜血并晕倒在地了!gcc居然生成了这么啰嗦的代码!但是,我们还是先说说C语言的函数调用规范吧。

我们已经看到了,参数从右到左依次入栈。下面的说明全部以32Bytes代码为准,其规范具体可罗列以下几条:

l          调用者负责将参数压入堆栈,顺序为从右到左依次入栈。也就是左边的最后入栈。

l          调用者使用near call指令将控制权传给被调用者。

l          被调用者得到控制权,一般需要创建堆栈框架(这不是必需的,通常都是这么做)。首先,将ebp压入堆栈保存,再将esp放入ebp,使ebp成为访问参数的基址指针。

l          被调用者通过ebp访问参数。因为ebp已经先行压入堆栈,所以[ebp+4]就是被call指令自动压入堆栈的返回地址,显然,从[ebp+8]开始,就是参数。由于函数最左边的参数最后被压入堆栈,所以[ebp+8]就是该参数,其他参数以此类推。像printf这样的函数,具有个数不确定的参数,但是参数入栈顺序的规则,说明被调用者通过[ebp+8]就能够找到第一个参数,其他参数的类型和数目,则需要由第一个参数给出。

l          被调用者减小esp的值为堆栈中的临时变量分配空间,然后使用ebp和一个负的偏移访问。

l          被调用者使用alaxeax返回大小不同的值。浮点数可以通过ST0寄存器返回。

l          被调用者完成处理后,使用事先建立的堆栈框架,恢复espsbp的值,并使用ret指令返回调用者。

l          调用者重新得到控制权,通过给esp加上一个立即数清空堆栈(尽量不要使用多次pop指令清空堆栈)。如果因为使用了错误的函数原型通过堆栈多传递了或者少传递了参数,调用者仍然能够将堆栈恢复到正确的状态,因为调用者知道自己向堆栈压了几个字节的数据。

结合C语言的函数调用规则,上面的代码不难理解。

80386开始,push指令的操作数可以是8-bit16-bit32-bit,但是C语言统统按32-bit整型数处理,被调用者也按32-bit进行处理。这一点很重要,特别是汇编语言和C语言混合编程时。

9.         基本的数据类型间的转换

gcc处理三类基本数据类型:

l          signed char , unsigned char  , 1 Byte

l          signed short , unsigned short , 2 Bytes

l          signed int , unsigned int , 4 Bytes

各种数据类型间的转换,遵循一般C语言的规则,具体可以参考IA-32的标准。这里只举一例说明:

int main()

{

char ch=’a’;

int x=2;

int y=-4;

}

使用同样的方法进行编译及反汇编:

00000000

:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 18                    sub    $0x18,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c6 45 ff 61                   movb   $0x61,0xffffffff(%ebp)

  14:          c7 45 f8 02 00 00 00   movl   $0x2,0xfffffff8(%ebp)

  1b:          c7 45 f4 fc ff ff ff        movl   $0xfffffffc,0xfffffff4(%ebp)

  22:          c9                        leave 

  23:          c3                        ret   

 

10.     gcc编译代码的基本运行环境

这一部分,我查了很多的文档,都没有这方面的介绍。又请教了很多的高手,大致情况如下,我实在无法保证这里所说的都是正确的,并且将来也是正确的,仅供参考:

l          32-bit的保护模式下运行。

l          段寄存器csdsesfsgsss必须指向同一段内存区域。

l          没有初始化的全局变量被放在BSS的段内,该区域在代码段之后。但是,如果你生成的文件是二进制文件,BSS段不是该文件的一部分,你需要自己小心使用。初始化的全局变量在DATA段内,它是二进制文件的一部分,并且位于代码段之后。被声明为const的全局变量被放在RODATA段内,它也是二进制文件的一部分,并放在代码段之后。

l          确保堆栈没有溢出,小心代码段和全局数据不要被破坏。

我也查了Intel提供的帮助文档“Intel Architecture Software Developer’s Manual”,一共有三卷之多!参考了其中关于内存组织(Volume 1:Memory Organization)中的说法(建议你去好好研究)。总之,使csdsss总是指向同一内存区域应该可以使代码正确运行。如果运行环境不是这样,我就不知道结果了。

11.     访问外部的全局变量

看看在非C语言程序中如何访问C语言程序中的全局变量。如果你想使用其他程序加载C程序,例如汇编语言写的程序,这部分很有用,特别是在核心开发时经常用到。

int myVal=0x5;

int main()

{

}

编译这段代码:

gcc –c test.c

ld –Ttext 0x0 –e main –N –oformat binary –Map memmap.txt –o test.bin test.o

objdump –D –b binrary –m i386 test.bin

得到如下结果:

00000000 <.data>:

   0:          55                        push   %ebp

   1:          89 e5                      mov    %esp,%ebp

   3:          83 ec 08                    sub    $0x8,%esp

   6:          83 e4 f0                     and    $0xfffffff0,%esp

   9:          b8 00 00 00 00                mov    $0x0,%eax

   e: 29 c4                      sub    %eax,%esp

  10:          c9                        leave 

  11:          c3                        ret   

  12:          00 00                      add    %al,(%eax)

  14:          05                        .byte 0x5

  15:          00 00                      add    %al,(%eax)

全局变量myVal存储在0x14。刚才已经使用-Map开关使ld生成了内存映像文件memmap.txt,应该能够找到:

.data           0x00000014        0x4

 *(.data .data.* .gnu.linkonce.d.*)

 .data          0x00000014        0x4 test.o

                0x00000014                myVal

说明myVal位于test.o模块的0x00000014位置。使用地址作为偏移量,就可以直接在其他语言中访问myVal变量了。另为也可以通过memmap.txt查到BSS段的大小:

cat memmap.txt | grep ‘/.bss’ | grep ‘0x’ | sed ‘s/.*0x/0x/’

本例子,BSS的大小是0x0

无法直接访问C程序中的使用static修饰的全局变量。因为这样的变量是静态的,map文件中没有列出他们的地址。也许你可以使用其他办法做到,但是,最好不要这样做。

12.     生成其他格式的二进制文件的选项

生成不同格式的二进制文件是一件相当麻烦的事。它需要使用很多不常使用的选项,并且有些在man的帮助信息中没有被列出。

首先是gcc的选项:-nostdinc。很显然,使用该选项后,gcc就不搜索默认的include路径了,通常是/usr/include。如果需要使用的自定义的头文件,可以使用-I选项添加搜索路径。

然后是ld的选项。第一个是-nostdlib,就是忽略标准库。如果需要,可以使用-L选项指定库的搜索路径。第二个是-Ttext,就是指定代码段的地址,如果没有继续指定其他段的地址,则他们将自动的一次被放在代码段之后。第三个是-e,就是指定代码的入口地址,默认的是_start,如果代码不是以其开头,就应该指定入口点。第四个是—oformat binary,就是输出的文件是原始的二进制文件,而是如文件可以使系统支持的任何文件。但是,中间模块文件不能是原始的二进制文件,因为还需要很多符号和重定位信息。可以使用—iformat选项指定输入文件的格式,但通常很少使用。第五个是-static,如果使用了其他库,用该使用静态链接方式,除非你的程序支持动态链接。

另外还有代码指示伪指令。汇编器可以编译16-bit代码,也可以编译32-bit代码。但是,gcc总是生成32-bit的汇编代码。通过在C代码中使用asm()伪指令可以让gcc生成16-bit汇编代码。

第一个是.code16,即生成在16-bit段中运行的16-bit代码;

第二个是.code32,即生成在32-bit段中运行的32-bit代码,默认情况下gcc总是这么做;

第三个是.code16gccgcc将根据需要决定生成在16-bit段下运行的16-bit32-bit代码。GAS将会加上必要的前缀,指示32-bit的指令或寄存器等。这个选项是很有用的,它允许我们使用C语言写在16-bit环境下运行的代码,不论是实模式还是保护模式。

现在可以在一个C模块中既有16-bit代码,又有32-bit代码,但是此时需要注意不同部分代码的地址空间问题。

例如,我们想使用gcc生成在DOS下运行的.com程序和启动引导程序。

首先,DOS中的.com文件是在实模式下运行的原始的二进制文件,其起始地址为0x100。要使用gcc生成.com文件,在每一个.c文件的开头加上如下伪指令:

__asm__(“code16gcc/n”);

如果需要引用其他库文件,则这些库文件也需要按这种方式生成。在链接时,加上如下选项:

-Ttext 0x100 –static –oformat binary

如果程序中包含嵌入的汇编代码,需要将其转换为AT&T格式。

如果要写引导程序,只需要在链接时使用0x7C00代替0x100!另外,最终生成的二进制代码必须小于446个字节!

13.     参考资料

l          Intel Architecture Software Developer’s Manual

l          Manual Pages in Linux

l          Redhat GNUPro Toolkit

 

你可能感兴趣的:(gcc,汇编,c,语言,编译器,linux)