编译器:理解小程序生成的汇编代码

时间:2023-01-14 03:15:41

I'm self-studying how compilers works. I'm learning by reading the disassembly of GCC generated code from small 64-bit Linux programs.

我正在自学编译器的工作原理。我正在阅读从小型64位Linux程序分解GCC生成的代码。

I wrote this C program:

我写了这个C程序:

#include <stdio.h>

int main()
{
    for(int i=0;i<10;i++){
        int k=0;
    }
}

After using objdump I get:

使用objdump后,我得到:

00000000004004d6 <main>:
  4004d6:       55                      push   rbp
  4004d7:       48 89 e5                mov    rbp,rsp
  4004da:       c7 45 f8 00 00 00 00    mov    DWORD PTR [rbp-0x8],0x0
  4004e1:       eb 0b                   jmp    4004ee <main+0x18>
  4004e3:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
  4004ea:       83 45 f8 01             add    DWORD PTR [rbp-0x8],0x1
  4004ee:       83 7d f8 09             cmp    DWORD PTR [rbp-0x8],0x9
  4004f2:       7e ef                   jle    4004e3 <main+0xd>
  4004f4:       b8 00 00 00 00          mov    eax,0x0
  4004f9:       5d                      pop    rbp
  4004fa:       c3                      ret    
  4004fb:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

Now I have some doubts.

现在我有一些疑问。

  1. What is that NOP at the end for, and why is it there? (alignment?)

    最后的NOP是什么,为什么会有?(对齐?)

  2. I'm compiling with gcc -Wall <program.c>. Why am I not getting the warning control reaches end of non-void function?

    我正在用gcc -Wall 编译。为什么我没有得到报警控制到达无虚函数的末端?

  3. Why doesn't the compiler allocate space on the stack with sub rsp,0x10? Why doesn't it use the rbp register for referencing local stack data?

    为什么编译器不使用子rsp 0x10在堆栈上分配空间呢?为什么它不使用rbp寄存器来引用本地堆栈数据?

    PS: If I call a function (like printf) in the for loop, why does the compiler suddenly generate sub rsp,0x10? Why does it still references local data with the rsp register. I expect the generated code to reference local stack data with rbp!

    PS:如果我在for循环中调用一个函数(如printf),为什么编译器会突然生成sub rsp,0x10?为什么它仍然使用rsp寄存器引用本地数据。我希望生成的代码可以使用rbp引用本地堆栈数据!

3 个解决方案

#1


12  

Regarding the second question, since the C99 standard it's allowed to not have an explicit return 0 in the main function, the compiler will add it implicitly. Note that this is only for the main function, no other function.

关于第二个问题,由于C99标准允许在主函数中没有显式的返回0,编译器将隐式地添加它。注意,这只适用于主函数,不适用于其他函数。

As for the third question, the rbp register acts as the frame pointer.

对于第三个问题,rbp寄存器充当框架指针。

Lastly the PS. It's likely that the called function is using 16 bytes (0x10) for the arguments passed to the function. The subtraction is what "removes" those variables from the stack. Could it possibly be two pointers you pass as arguments?

最后是PS。对于传递给函数的参数,调用函数很可能使用了16个字节(0x10)。减法是从堆栈中“删除”这些变量的方法。可能是作为参数传递的两个指针吗?

If you're serious learning how compilers in general works, and possibly want to create your own (it's fun! :)), then I suggest you invest in some books about the theory and practice of it. The dragon book is an excellent addition to any programmers bookshelf.

如果您正在认真学习编译器的一般工作原理,并且希望创建自己的编译器(这很有趣!)),那我建议你买一些有关它的理论和实践的书。《龙之书》是任何程序员书架上的优秀作品。

#2


6  

  1. Yes, the nop is for alignment. Compilers use different instructions for different lengths of padding needed, knowing that modern CPU will be pre-fetching and decoding several instructions ahead.

    是的,nop是对齐的。编译器对所需的填充长度使用不同的指令,知道现代CPU将预先获取并解码前面的几个指令。

  2. As others have said, the C99 standard returns 0 from main() by default if there's no explicit return statement (see 5.1.2.2.3 in C99 TC3), so no warning is raised.

    正如其他人所说,如果没有显式返回语句(参见C99 TC3中的5.1.2.2.3),那么C99标准将默认返回main()中的0,因此不会引起任何警告。

  3. The 64-bit System V Linux ABI reserves a 128-byte "red zone" below the current stack pointer that leaf functions (functions that do not call any other functions - and your main() is one such) can use for local variables and other scratch values without having to sub rsp / add rsp. And so rbp == rsp.

    64位系统V Linux ABI储备一个128字节的“红灯区”低于当前堆栈指针叶功能(功能不调用任何其他功能,你的主要()就是这样一个)可以使用本地变量和其他划痕值而无需子负责/添加负责。所以rbp == rsp。

And for the PS: when you call a function in the for() loop (or anywhere in your main()), main() is no longer a leaf function, so the compiler can no longer use the red zone. That's why the it allocates space on the stack with sub rsp, 0x10. However, it knows the relationship between rsp and rbp, so it can use either when accessing data.

对于PS:当您调用for()循环中的函数(或main()中的任何地方)时,main()不再是叶子函数,因此编译器不能再使用红色区域。这就是为什么it用子rsp 0x10分配堆栈上的空间。但是,它知道rsp和rbp之间的关系,因此在访问数据时可以使用其中任何一种。

#3


6  

Anything after the ret cannot be relied on to be code. Decoding as nop means "No OPeration"

在ret之后的任何东西都不能依赖于代码。解码为nop表示“无操作”

The 2nd point is the compiler detecting you leave the main function without returning a value and it inserts a return 0 (only defined for main).

第二点是编译器检测你离开主函数而不返回一个值,它插入一个返回0(仅为main定义)。

The rbp register, with bp meaning "Base Pointer", points to the stack frame of the currect function. A function call often results in the function entry saving rbp and using the current value of rsp for rbp. Fetching/storing function arguments and local variables are done relative to rbp.

rbp寄存器,带有bp的意思是“基指针”,指向currect函数的堆栈帧。函数调用通常会节省rbp,并使用rsp的当前值来调用rbp。获取/存储函数参数和局部变量相对于rbp。


I think your third question needs some more attention, " Why doesn't the compiler allocate space on the stack with sub rsp,0x10? Why doesn't it use the rbp register for referencing local stack data?"

Actually, the compiler does allocate space on the stack. But it does not change the stackpointer. It can do that because the functon calls no other functions. It just uses space below the curent sp (the stack grows down) and it uses rbp to access i ([rbp-0x8]) and k ([rbp-0x4]).

实际上,编译器在堆栈上分配空间。但它不会改变堆栈指针。它可以这样做,因为函子不调用其他函数。它只使用curent sp(堆栈变小)下面的空间,并使用rbp访问i ([rbp-0x8])和k ([rbp-0x4])。


I must add the following note: not adjusting sp for the use of local variables seems not interrupt safe and so the compiler relies on the hardware automatically switching to a system stack when interrupts occur. Otherwise, the first interrupt that came along would push the instruction pointer onto the stack and would overwrite the local variable.

Question of interrupts solved in Compiler using local variables without adjusting RSP

在不调整RSP的情况下,使用局部变量解决了在编译器中中断的问题。

#1


12  

Regarding the second question, since the C99 standard it's allowed to not have an explicit return 0 in the main function, the compiler will add it implicitly. Note that this is only for the main function, no other function.

关于第二个问题,由于C99标准允许在主函数中没有显式的返回0,编译器将隐式地添加它。注意,这只适用于主函数,不适用于其他函数。

As for the third question, the rbp register acts as the frame pointer.

对于第三个问题,rbp寄存器充当框架指针。

Lastly the PS. It's likely that the called function is using 16 bytes (0x10) for the arguments passed to the function. The subtraction is what "removes" those variables from the stack. Could it possibly be two pointers you pass as arguments?

最后是PS。对于传递给函数的参数,调用函数很可能使用了16个字节(0x10)。减法是从堆栈中“删除”这些变量的方法。可能是作为参数传递的两个指针吗?

If you're serious learning how compilers in general works, and possibly want to create your own (it's fun! :)), then I suggest you invest in some books about the theory and practice of it. The dragon book is an excellent addition to any programmers bookshelf.

如果您正在认真学习编译器的一般工作原理,并且希望创建自己的编译器(这很有趣!)),那我建议你买一些有关它的理论和实践的书。《龙之书》是任何程序员书架上的优秀作品。

#2


6  

  1. Yes, the nop is for alignment. Compilers use different instructions for different lengths of padding needed, knowing that modern CPU will be pre-fetching and decoding several instructions ahead.

    是的,nop是对齐的。编译器对所需的填充长度使用不同的指令,知道现代CPU将预先获取并解码前面的几个指令。

  2. As others have said, the C99 standard returns 0 from main() by default if there's no explicit return statement (see 5.1.2.2.3 in C99 TC3), so no warning is raised.

    正如其他人所说,如果没有显式返回语句(参见C99 TC3中的5.1.2.2.3),那么C99标准将默认返回main()中的0,因此不会引起任何警告。

  3. The 64-bit System V Linux ABI reserves a 128-byte "red zone" below the current stack pointer that leaf functions (functions that do not call any other functions - and your main() is one such) can use for local variables and other scratch values without having to sub rsp / add rsp. And so rbp == rsp.

    64位系统V Linux ABI储备一个128字节的“红灯区”低于当前堆栈指针叶功能(功能不调用任何其他功能,你的主要()就是这样一个)可以使用本地变量和其他划痕值而无需子负责/添加负责。所以rbp == rsp。

And for the PS: when you call a function in the for() loop (or anywhere in your main()), main() is no longer a leaf function, so the compiler can no longer use the red zone. That's why the it allocates space on the stack with sub rsp, 0x10. However, it knows the relationship between rsp and rbp, so it can use either when accessing data.

对于PS:当您调用for()循环中的函数(或main()中的任何地方)时,main()不再是叶子函数,因此编译器不能再使用红色区域。这就是为什么it用子rsp 0x10分配堆栈上的空间。但是,它知道rsp和rbp之间的关系,因此在访问数据时可以使用其中任何一种。

#3


6  

Anything after the ret cannot be relied on to be code. Decoding as nop means "No OPeration"

在ret之后的任何东西都不能依赖于代码。解码为nop表示“无操作”

The 2nd point is the compiler detecting you leave the main function without returning a value and it inserts a return 0 (only defined for main).

第二点是编译器检测你离开主函数而不返回一个值,它插入一个返回0(仅为main定义)。

The rbp register, with bp meaning "Base Pointer", points to the stack frame of the currect function. A function call often results in the function entry saving rbp and using the current value of rsp for rbp. Fetching/storing function arguments and local variables are done relative to rbp.

rbp寄存器,带有bp的意思是“基指针”,指向currect函数的堆栈帧。函数调用通常会节省rbp,并使用rsp的当前值来调用rbp。获取/存储函数参数和局部变量相对于rbp。


I think your third question needs some more attention, " Why doesn't the compiler allocate space on the stack with sub rsp,0x10? Why doesn't it use the rbp register for referencing local stack data?"

Actually, the compiler does allocate space on the stack. But it does not change the stackpointer. It can do that because the functon calls no other functions. It just uses space below the curent sp (the stack grows down) and it uses rbp to access i ([rbp-0x8]) and k ([rbp-0x4]).

实际上,编译器在堆栈上分配空间。但它不会改变堆栈指针。它可以这样做,因为函子不调用其他函数。它只使用curent sp(堆栈变小)下面的空间,并使用rbp访问i ([rbp-0x8])和k ([rbp-0x4])。


I must add the following note: not adjusting sp for the use of local variables seems not interrupt safe and so the compiler relies on the hardware automatically switching to a system stack when interrupts occur. Otherwise, the first interrupt that came along would push the instruction pointer onto the stack and would overwrite the local variable.

Question of interrupts solved in Compiler using local variables without adjusting RSP

在不调整RSP的情况下,使用局部变量解决了在编译器中中断的问题。