c - gcc在调用前从esp中减去

标签 c gcc assembly x86 stack

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

我有两个名为 main.c 的 C 文件和 hello.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

主文件
extern void hello();


void main(){
    hello();
}

你好ç
void hello(){}

脚本文件
SECTIONS{

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

最佳答案

正如我在评论中提到的:

The first few lines (plus the push ecx) are to ensure the stack is aligned on a 16-byte boundary which is required by the Linux System V i386 ABI. The pop ecx and lea before the ret in main is to undo that alignment work.



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

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

There are so many pitfalls in doing this that I recommend against it.



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

-mpreferred-stack-boundary=num

Attempt to keep the stack boundary aligned to a 2 raised to num byte boundary. If -mpreferred-stack-boundary is not specified, the default is 4 (16 bytes or 128 bits).



如果我们将它添加到您的 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 ebp 的形式和 mov ebp,esp pop ebp .我们可以使用 -fomit-frame-pointer 删除这些在 GCC manual 中定义作为:

The option -fomit-frame-pointer removes the frame pointer for all functions which might make debugging harder.



如果我们添加该选项,我们会得到 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

Optimize for size. -Os enables all -O2 optimizations that do not typically increase code size. It also performs further optimizations designed to reduce code size.



这有一个副作用 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 (number)

On the Intel 386, the regparm attribute causes the compiler to pass arguments number one to number if they are of integral type in registers EAX, EDX, and ECX instead of on the stack. Functions that take a variable number of arguments will continue to be passed all of their arguments on the stack.



我用使用 __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有关于 build 一的信息。它也有关于原因的说法:

Why do I need a Cross Compiler?

You need to use a cross-compiler unless you are developing on your own operating system. The compiler must know the correct target platform (CPU, operating system), otherwise you will run into trouble. If you use the compiler that comes with your system, then the compiler won't know it is compiling something else entirely. Some tutorials suggest using your system compiler and passing a lot of problematic options to the compiler. This will certainly give you a lot of problems in the future and the solution is build a cross-compiler.

关于c - gcc在调用前从esp中减去,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43596226/

相关文章:

c - double* 和 double 到二元运算符&类型的无效操作数

将字符从字符串复制到C中的另一个字符串

c - Freeimage ANSI C Codeblock Windows - 链接 - 新手

c - qsort死循环导致错误C

c++ - g++ 和 gcc 4.8 找不到 null_ptr?

linux - 在 32 位系统中对 64 位字的操作

assembly - 如何在 gdb 中给定地址处的汇编指令上中断?

assembly - 汇编语言中 "ds:"的含义

assembly - x86组装-将rax夹紧到[0 .. limit)的优化

gcc - 内联汇编错误,阻止 gcc 编译尝试