gcc在调用前从esp中减去

时间:2017-04-24 19:26:03

标签: c gcc assembly x86 stack

我打算用 C 编写一个小内核,我真的不希望它因不必要的指令而膨胀。

我有两个名为main.chello.c C 文件。我使用以下 GCC 命令编译并链接它们:

gcc -Wall -T lscript.ld -m16 -nostdlib main.c hello.c -o main.o

我正在使用以下 OBJDUMP 命令转储.text部分:

objdump -w -j .text -D -mi386 -Maddr16,data16,intel main.o

并获得以下转储:

00001000 <main>:
    1000:   67 66 8d 4c 24 04       lea    ecx,[esp+0x4]
    1006:   66 83 e4 f0             and    esp,0xfffffff0
    100a:   67 66 ff 71 fc          push   DWORD PTR [ecx-0x4]
    100f:   66 55                   push   ebp
    1011:   66 89 e5                mov    ebp,esp
    1014:   66 51                   push   ecx
    1016:   66 83 ec 04             sub    esp,0x4
    101a:   66 e8 10 00 00 00       call   1030 <hello>
    1020:   90                      nop
    1021:   66 83 c4 04             add    esp,0x4
    1025:   66 59                   pop    ecx
    1027:   66 5d                   pop    ebp
    1029:   67 66 8d 61 fc          lea    esp,[ecx-0x4]
    102e:   66 c3                   ret    
00001030 <hello>:
    1030:   66 55                   push   ebp
    1032:   66 89 e5                mov    ebp,esp
    1035:   90                      nop
    1036:   66 5d                   pop    ebp
    1038:   66 c3                   ret  

我的问题是:为什么生成以下行的机器代码? 我可以看到减法和加法相互完成,但它们为什么会生成?我没有任何要在堆栈上分配的变量。我很欣赏有关 ECX 的使用情况的资料来源。

1016:   66 83 ec 04             sub    esp,0x4
1021:   66 83 c4 04             add    esp,0x4

的main.c

extern void hello();


void main(){
    hello();
}

的hello.c

void hello(){}

lscript.ld

SECTIONS{

    .text 0x1000 : {*(.text)}
}

1 个答案:

答案 0 :(得分:10)

正如我在评论中提到的那样:

  

前几行(加上push ecx)是为了确保堆栈在Linux System V i386 ABI所需的16字节边界上对齐。主要pop ecx之前的learet是撤消对齐工作的。

@RossRidge提供了另一个Stackoverflow answer的链接,详细说明了这一点。

在这种情况下,您似乎正在进行真正的模式开发。 GCC 并不适合这个,但它可以工作,我会假设你知道你在做什么。我在这个Stackoverflow answer中提到了使用-m16的一些陷阱。我在关于使用 GCC 的实模式开发的答案中提出了这个警告:

  

这样做有很多陷阱我建议反对。

如果你仍未被阻止并希望继续前进,你可以做一些事情来最小化代码。在进行函数调用时,堆栈的16字节对齐是最近Linux System V i386 ABIs的一部分。由于您要为非Linux环境生成代码,因此可以使用编译器选项-mpreferred-stack-boundary=2将堆栈对齐更改为4。 GCC manual说:

  

-mpreferred堆栈边界= NUM​​

     

尝试保持堆栈边界对齐2到num字节边界。如果未指定-mpreferred-stack-boundary,则默认值为4(16字节或128位)。

如果我们将其添加到您的 GCC 命令,我们会得到gcc -Wall -T lscript.ld -m16 -nostdlib main.c hello.c -o main.o -mpreferred-stack-boundary=2

00001000 <main>:
    1000:       66 55                   push   ebp
    1002:       66 89 e5                mov    ebp,esp
    1005:       66 e8 04 00 00 00       call   100f <hello>
    100b:       66 5d                   pop    ebp
    100d:       66 c3                   ret

0000100f <hello>:
    100f:       66 55                   push   ebp
    1011:       66 89 e5                mov    ebp,esp
    1014:       66 5d                   pop    ebp
    1016:       66 c3                   ret

现在,在16字节边界上获取它的所有额外对齐代码都已消失。我们留下了典型的功能框架指针序言和结尾代码。这通常采用push ebpmov ebp,esp pop ebp的形式。我们可以使用GCC manual中的-fomit-frame-pointer定义将其删除为:

  

选项-fomit-frame-pointer删除所有可能使调试更难的函数的帧指针。

如果我们添加该选项,我们会获得gcc -Wall -T lscript.ld -m16 -nostdlib main.c hello.c -o main.o -mpreferred-stack-boundary=2 -fomit-frame-pointer

00001000 <main>:
    1000:       66 e8 02 00 00 00       call   1008 <hello>
    1006:       66 c3                   ret

00001008 <hello>:
    1008:       66 c3                   ret

然后,您可以使用-Os优化尺寸。 GCC 手册说明了这一点:

  

-Os

     

优化尺寸。 -Os启用所有通常不会增加代码大小的-O2优化。它还执行旨在减少代码大小的进一步优化。

这会产生副作用,main将放入名为.text.startup的部分。如果我们用objdump -w -j .text -j .text.startup -D -mi386 -Maddr16,data16,intel main.o显示两者,我们会得到:

Disassembly of section .text:

00001000 <hello>:
    1000:       66 c3                   ret

Disassembly of section .text.startup:

00001002 <main>:
    1002:       e9 fb ff                jmp    1000 <hello>

如果在单独的对象中有函数,则可以更改调用约定,因此前3个Integer类参数将在寄存器而不是堆栈中传递。 Linux内核也使用这种方法。有关这方面的信息,请参阅GCC documentation

  

regparm(号码)

     

在Intel 386上,如果regparm属性在寄存器EAX,EDX和ECX中是整数类型而不是在堆栈上,则regparm属性会使编译器将第一个参数传递给数字。采用可变数量参数的函数将继续传递给堆栈中的所有参数。

我用代码使用__attribute__((regparm(3)))编写了一个Stackoverflow答案,这些代码可能是进一步信息的有用来源。

其他建议

我建议您考虑单独编译每个对象而不是完全编译。这也是有利的,因为稍后可以在Makefile中更容易地完成。

如果我们使用上面提到的额外选项查看您的命令行,您可以:

gcc -Wall -T lscript.ld -m16 -nostdlib main.c hello.c -o main.o \
    -mpreferred-stack-boundary=2 -fomit-frame-pointer -Os

我建议你这样做:

gcc -c -Os -Wall -m16 -ffreestanding -nostdlib -mpreferred-stack-boundary=2 \
    -fomit-frame-pointer main.c -o main.o
gcc -c -Os -Wall -m16 -ffreestanding -nostdlib -mpreferred-stack-boundary=2 \
    -fomit-frame-pointer hello.c -o hello.o

-c选项(我将其添加到开头)强制编译器只从源生成目标文件而不执行链接。您还会注意到-T lscript.ld已被删除。我们在上面创建了.o个文件。我们现在可以使用 GCC 将所有这些链接在一起:

gcc -ffreestanding -nostdlib -Wl,--build-id=none -m16 \
    -Tlscript.ld main.o hello.o -o main.elf

-ffreestanding将强制链接器不使用 C 运行时,-Wl,--build-id=none将告诉编译器不要在可执行文件中为构建注释生成一些噪声。为了使其真正起作用,您需要一个稍微复杂的链接描述文件,将.text.startup置于.text之前。此脚本还会添加.data部分,.rodata.bss部分。 DISCARD 选项可删除异常处理数据和其他不需要的信息。

ENTRY(main)
SECTIONS{

    .text 0x1000 : SUBALIGN(4) {
        *(.text.startup);
        *(.text);
    }
    .data : SUBALIGN(4) {
        *(.data);
        *(.rodata);
    }
    .bss : SUBALIGN(4) {
        __bss_start = .;
        *(COMMON);
        *(.bss);
    }
    . = ALIGN(4);
    __bss_end = .;

    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
        *(.note.gnu.build-id);
    }
}

如果我们使用objdump -w -D -mi386 -Maddr16,data16,intel main.elf查看完整的 OBJDUMP ,我们会看到:

Disassembly of section .text:

00001000 <main>:
    1000:       e9 01 00                jmp    1004 <hello>
    1003:       90                      nop

00001004 <hello>:
    1004:       66 c3                   ret

如果要将main.elf转换为可放入磁盘映像并读取的二进制文件(即通过BIOS中断0x13),可以这样创建:

objcopy -O binary main.elf main.bin

如果使用main.bin使用 NDISASM 转储ndisasm -b16 -o 0x1000 main.bin,您会看到:

00001000  E90100            jmp word 0x1004
00001003  90                nop
00001004  66C3              o32 ret

交叉编译器

我不能强调这一点,但你应该考虑使用 GCC 交叉编译器。 OSDev Wiki有关于构建一个的信息。它还有这个说明原因:

  

为什么我需要交叉编译器?

     

除非您在自己的操作系统上进行开发,否则您需要使用交叉编译器。编译器必须知道正确的目标平台(CPU,操作系统),否则会遇到麻烦。如果您使用系统附带的编译器,那么编译器就不会知道它正在完全编译其他内容。一些教程建议使用您的系统编译器并将许多有问题的选项传递给编译器。这肯定会在将来给你带来很多问题,解决方案就是构建一个交叉编译器。