Previously, I wrote a cmm interpreter and designed and implemented function calls. At the time of design, I didn't look at how the c compiler was implemented. But today, after reading a deep understanding of computer systems, I found that my design method is really similar to c's, now, let's simply summarize it. because it involves a lot of compilation knowledge, we 'd better take a look at the compilation of children's shoes that do not understand Compilation:
First of all, I want to talk about the most important idea, that is, the idea of relative addressing. Without this method, it is almost impossible to call the compiling process. Because computer commands use the address of the variable as the operand, it means that you must calculate the address of the variable before generating code. However, process calls are sometimes uncertain, such as recursion. you are not sure how many times it will be called before it returns. Naturally, the number of local ratio variables generated is also uncertain, obviously, we cannot use absolute addresses in operands. The practice is to set a base address register, which is ebp in c, which records the base address of the stack at the beginning of the call process. With this, we only need to determine the address at the beginning of the relative process call of each variable in the process call code, no matter how many times we call the code block of this process, the stack will grow in blocks, and the relative address in the process will not change.
Let me briefly describe my design:
When I call each process, I need to save the old ebp to the bottom of the new stack frame, and then save the return address to the bottom of the stack frame by using the backfilling technology, in this way, each time a process is called, the bottom of the stack frame must be the two values. Then the new stack frame pointer is the stack location of the old ebp. Of course, when we update the ebp, The ADD method is used to accumulate the New Frame pointer, add the value in the ebp register to the maximum offset of the current relative position.
Then we press the parameters in the process. I press each value directly, that is, all process calls are passed through the value method. This is also the limitation of my design. After all the parameters are pushed to the stack, if you use them later, you can view the information about function parameters in the symbol table and find out their Parameter order, then convert them to their relative addresses through computation. If there is a local variable declaration, the function will be stored in the stack relay allocation space of the function.
The return Statement is returned. Before returning the ebp, you must store the returned address in the Temporary Variable Area. Otherwise, the ebp cannot be obtained after it is changed, you also need to load the return value to eax to assign values after the return value.
My design can indeed implement process calls, including recursion. Let's take a look at the design of the C compiler. I am a reference book and can understand it by viewing the disassembly code:
First, let's take a look at the online activity records for process calls:
Obviously, the c compiler and my design are different in the stack storage structure, and the biggest difference is that when a process call occurs, it stores the parameters and return addresses of the called functions in the caller's stack frame. Because stack frames increase from high addresses to low addresses, this causes the corresponding address to be set to a positive value when the parameter value is accessed during the calling process, this is because the ebp has been updated. In addition, the esp register is used in C to save the absolute address at the top of the current stack (according to my idea ). To better understand the calling process of this process, let's look at the following assembly code.
Caller: main {
Int start = 9;
Int stride = start + 2;
Char * tmp = extract_message1 (start, stride );
}
Called by: char * extract_message1 (int start, int stride) {... return "cao ";}
So let's look at what happened before and after the process call through the Anti-Assembler: (the assembly code irrelevant to the process call has been deleted)
First, in the external part of the process call:
00A5183C mov eax, dword ptr [stride]
00A5183F push eax
00A51840 mov ecx, dword ptr [start]
00A51843 push ecx
00A51844 call extract_message1 (0a000022h)
00A51849 add esp, 8
00A5184C mov dword ptr [tmp], eax
It is clear from this Assembly Code. The first step is to press the function parameter from right to left. Then, the call command is used to initiate a function call and jump to the function for execution. After the result is returned, the following command will continue to be executed. Note that this add command has obvious intention, which is equivalent to releasing the parameter space. However, you must note that after adding, we can still access them. Finally, execute the value assignment statement to move the returned values in eax to the storage space of the variable tmp.
So what happened in the call command?
See the following assembly code:
00A515A0 push ebp
00A515A1 mov ebp, esp
00A515A3 sub esp, 0F0h
00A515A9 push ebx
00A515AA push esi
00A515AB push edi
00A515AC lea edi, [ebp-0F0h]
00A515B2 mov ecx, 3Ch
00A515B7 mov eax, 0 CCCCCCCCh
The above code occurs after the call command is executed. In fact, the call command has done two parts. The first part is to press the return address into the stack, and the second part is to jump to the start of the process for execution. The two operations are related to pc registers. The next step is the preparation for the process establishment. It can be seen from the assembly code. First, save the ebp and set a new ebp. The new value is the current esp value. Note that I directly update ebp, that is, the absolute address is constantly updated in ebp, and c clearly shows that the update of its absolute value is through esp, updating ebp is based on the absolute address in esp. After the update, allocate a piece of space (I am not sure what it is, but esp has changed at this time, which means the top of the stack has been moved up again ), the caller saves the registers.
Here we will briefly describe the caller's storage register and the caller's storage register. The caller needs to push these registers to their stack frames before calling them. That is to say, it is not sure whether the sub-process will use these registers, so it is all pushed for insurance. While the caller saves the registers, the caller does not need to save them. If these registers are used during the call process, they need to push them to the stack frame of the sub-process, then overwrite. After the sub-process returns, you need to restore these values again. If the sub-process does not use these registers, you do not need to read or write stack frames. Therefore, this method can potentially reduce the number of reads and writes to stack frames. Therefore, the C compiler adopts the next method. In the Ia32 machine, the caller saves eax, ecx, and edx registers, while the caller saves ebx, esi, and edi registers. so you should understand the above three push statements. The last three commands are initialization. Ignore it for the moment.
When a return statement is encountered, check the following Assembly:
00A5164E pop edi
00A5164F pop esi
00A51650 pop ebx
00A51651 mov esp, ebp
00A51653 pop ebp
00A51654 ret
It can be seen that after the caller saves the register, the computer restores the ebp value. The pop ebp saves the old ebp to the ebp register. The last ret statement is to pop the returned address and set the pc value to that.
From the above, we can see that the ideas I designed are the same as those of c. They are both relative, jump, and stack frames. So from this point of view, at least my design is correct.