Spl*_*ash 3 c x86 assembly stack gcc
我打算用C来编写一个小内核,我真的不希望它因不必要的指令而膨胀.
我有两个ç这是所谓的文件main.c
和hello.c
.我使用以下GCC命令编译并链接它们:
gcc -Wall -T lscript.ld -m16 -nostdlib main.c hello.c -o main.o
Run Code Online (Sandbox Code Playgroud)
我正在使用以下OBJDUMP命令转储.text部分:
objdump -w -j .text -D -mi386 -Maddr16,data16,intel main.o
Run Code Online (Sandbox Code Playgroud)
并获得以下转储:
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
Run Code Online (Sandbox Code Playgroud)
我的问题是:为什么生成以下行的机器代码?我可以看到减法和加法相互完成,但它们为什么会生成?我没有任何要在堆栈上分配的变量.我很欣赏有关ECX使用情况的资料来源.
1016: 66 83 ec 04 sub esp,0x4
1021: 66 83 c4 04 add esp,0x4
Run Code Online (Sandbox Code Playgroud)
main.c中
extern void hello();
void main(){
hello();
}
Run Code Online (Sandbox Code Playgroud)
你好ç
void hello(){}
Run Code Online (Sandbox Code Playgroud)
lscript.ld
SECTIONS{
.text 0x1000 : {*(.text)}
}
Run Code Online (Sandbox Code Playgroud)
Mic*_*tch 10
正如我在评论中提到的那样:
前几行(加上push ecx)是为了确保堆栈在16字节边界上对齐,这是Linux System V i386 ABI所要求的.在
pop ecx
与lea
之前ret
的主要是撤消校准工作.
@RossRidge提供了另一个Stackoverflow答案的链接,详细说明了这一点.
在这种情况下,您似乎正在进行真正的模式开发.海湾合作委员会不适合这个,但它可以工作,我会假设你知道你在做什么.我-m16
在Stackoverflow的回答中提到了一些使用的陷阱.我在关于GCC实模式开发的答案中提出了这个警告:
这样做有很多陷阱,我建议不要这样做.
如果您仍未被阻止并希望继续前进,您可以做一些事情来最小化代码.在进行函数调用时,堆栈的16字节对齐是最近的一部分Linux System V i386 ABIs
.由于您要为非Linux环境生成代码,因此可以使用编译器选项将堆栈对齐更改为4 -mpreferred-stack-boundary=2
.在GCC手册说:
-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
Run Code Online (Sandbox Code Playgroud)
现在所有在16字节边界上获得它的额外对齐代码已经消失.我们留下了典型的功能框架指针序言和结尾代码.这通常是push ebp
和的形式mov ebp,esp
pop ebp
.我们可以使用GCC手册中的-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
Run Code Online (Sandbox Code Playgroud)
然后,您可以使用优化大小-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>
Run Code Online (Sandbox Code Playgroud)
如果在单独的对象中有函数,则可以更改调用约定,因此前3个Integer类参数将在寄存器而不是堆栈中传递.Linux内核也使用这种方法.有关这方面的信息可以在GCC文档中找到:
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
Run Code Online (Sandbox Code Playgroud)
我建议你这样做:
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
Run Code Online (Sandbox Code Playgroud)
该-c
选项(我把它添加到开头)强制编译器生成刚刚从源头上目标文件,而不是进行链接.您还会注意到-T lscript.ld
已删除.我们在.o
上面创建了文件.我们现在可以使用GCC将所有这些链接在一起:
gcc -ffreestanding -nostdlib -Wl,--build-id=none -m16 \
-Tlscript.ld main.o hello.o -o main.elf
Run Code Online (Sandbox Code Playgroud)
该-ffreestanding
会强制链接不使用Ç运行时,-Wl,--build-id=none
会告诉编译器不生成可执行的构建说明一些噪音.为了使其真正起作用,您需要一个稍微复杂的链接器脚本来放置.text.startup
之前的脚本.text
.此脚本还添加了.data
section,the .rodata
和.bss
sections.该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);
}
}
Run Code Online (Sandbox Code Playgroud)
如果我们看一个完整的OBJDUMP,objdump -w -D -mi386 -Maddr16,data16,intel main.elf
我们会看到:
Disassembly of section .text:
00001000 <main>:
1000: e9 01 00 jmp 1004 <hello>
1003: 90 nop
00001004 <hello>:
1004: 66 c3 ret
Run Code Online (Sandbox Code Playgroud)
如果要转换main.elf
为可以放入磁盘映像并读取它的二进制文件(即通过BIOS中断0x13),可以这样创建:
objcopy -O binary main.elf main.bin
Run Code Online (Sandbox Code Playgroud)
如果你倾倒main.bin
与NDISASM使用ndisasm -b16 -o 0x1000 main.bin
你会看到:
00001000 E90100 jmp word 0x1004
00001003 90 nop
00001004 66C3 o32 ret
Run Code Online (Sandbox Code Playgroud)
我不能强调这一点,但你应该考虑使用GCC交叉编译器.该OSDev维基对建设一个信息.它还有这个说明原因:
为什么我需要交叉编译器?
除非您在自己的操作系统上进行开发,否则您需要使用交叉编译器.编译器必须知道正确的目标平台(CPU,操作系统),否则会遇到麻烦.如果您使用系统附带的编译器,那么编译器将不会知道它正在完全编译其他内容.一些教程建议使用您的系统编译器并将许多有问题的选项传递给编译器.这肯定会在将来给你带来很多问题,解决方案是构建一个交叉编译器.