Detailed explanation of function call (function state saving parameter transfer and return value)

Understanding the program from the perspective of assembly-function call

We already know that the program is an instruction pipeline (PipeLine) that is executed sequentially . Branch and loop logic can be implemented by jumping backward or forward in the pipeline.
In fact, the function call is just a jump. When a function is called, it jumps to the beginning of the instruction stream of that function. After the function is executed, it jumps back.
The function can obtain externally written data (input), can hold its own unique data (local state), and can also write data (output) to the outside. Theoretically, the number of functions owned by a program is unlimited, but the number of registers is very small. How can infinite functions save their respective data through a limited number of registers?

1. How to realize function call and return by jumping

You only need to know the destination to jump. But if you want to jump back, then you need to save the address before returning to a place that can be read after the jump.
The assembly instruction call is used to save the jump-back address in memory and then jump, while ret can use the jump-back address saved by the call instruction to return from the function.
The call instruction is often followed by an address , just like the jmp instruction. But before the real jump, it saved the address of the next instruction next to the call instruction in memory M [% rsp].
With so many functions, is one% rsp enough? Based on the following two facts, we say: enough.

  • Fact 1: No matter how many functions there are, only one function is executed at a specific time. So only one% rsp is needed at a specific time. The question is how to derive this% rsp to other% rsp.
  • Fact 2: No matter how many functions there are, each executed function is called by the previous function. So we don't need to derive% rsp of all other functions from the current% rsp, we only need to know the% rsp of the previous function (caller). Is n’t this addition and subtraction?

For example,% rsp of the main function is x, then when function A calls function B,% rsp =% rsp + N, and when B returns,% rsp =% rsp-N.
Since% rsp is used for memory addressing, the constant N is 8.
This is the classic scaling model of stack memory.

Having said so much, let's look at the code:

400540: lea     2(%rdi), %rax
400544: ret

400545: sub     5, %rdi
400549: call    400540
40054e: add     %rax, %rax
400551: ret

40055b: call    400545
400560: mov     %rax, %rdx

If the value of% rsp is 0x7fffffffe820 when executing 40055b: call 400545, then after the call is executed, the value of% rsp will be reduced by 8 to become 0x7fffffffe818, and the value 0x400560 will be saved in the memory M [0x7fffffe818], which is the one below the call The address of the command mov. After that, the program jumps to 0x400545 and starts running.

At 0x400549, there is another call instruction, so the same logic comes again, the value of% rsp minus 8 is 0x7fffffff10, and the memory M [0x7fffffffe810] stores the address of the following add instruction 0x40054e. After that, the program jumps to 0x400540.

A ret instruction was encountered at 0x400544. At this time, the stack pointer address was read from% rsp as 0x7fffffffe810, and then the value of M [0x7fffffffe810] was read from 0x40054e. Then% rsp plus 8 restores to the value before the last call instruction, and jumps to 0x40054e, and starts running immediately after the last call instruction.

The ret process at 0x400551 is similar to the previous step and will not be repeated here.
The summary is to save the return position of the function through a stack pointer that expands and contracts synchronously with the function call, and it can both jump over and jump back.

Second, how to save the state of the function through the register

Most functions have parameters and return values. How to save these data through limited registers?
Simple, remember that registers have been assigned clear responsibilities,% rdi,% rsi,% rdx,% rcx,% r8,% r9 are used to save the 6 parameters of the function, and% rax is used to save the return of the function value.

In the calling function, save the parameters in the parameter register before executing the call instruction. In the called function, reading the value from the register is equivalent to which parameter is used (% rdi is the first,% rsi is the Two ... wait).

After the called function ret reaches the next instruction position of the call, the return value of the called function is saved in% rax.

However, this is still a problem. The return value is usually only one, but the parameters may not be only six. What should I do when the number of function parameters exceeds six?
At this time, it depends on the stack memory again.

Third, how to save more function state through memory

There are several situations that cause us to save the state of the function in memory:

  • When the number of parameters of the called function exceeds 6
  • The local data of the function exceeds the number of registers
  • The function local data uses an array, so you must use the array to access the data
  • The function uses the & operation on the local data to obtain the memory address, so the data must be saved to memory

In either case, we must use memory to store the data. These data are stored at a specific offset position based on the function stack pointer, which is collectively called stack memory (also because its scaling method is similar to the stack data structure).
Generally use push and pop instructions. The push instruction decrements the current stack pointer value R [% rsp] by 8, and then saves the value to be saved to memory M [R [% rsp]]. The pop pointer takes the memory value M [R [% rsp]] to the specified register, and then adds 8 to R [% rsp] to restore the stack pointer position.
Compile time guarantees that each push must correspond to a pop to ensure that R [% rsp] can be restored to the value at the beginning of the function call before the function returns. At the same time, developers do not have to worry about freeing up memory in the stack space, because it is automatically maintained during compilation.

3.1 Use stack memory to save more function parameters

When P calls Q, the parameters are first saved in the corresponding registers. If the number of registers is not enough, the remaining parameters will be saved to the stack memory using the push instruction. After preparing these parameters, call the call instruction.
When Q is executed, the first 6 parameters are read and written from the register as usual, and the subsequent ones are read and written according to the offset of M [R [% rsp] + 8n]. Note that the parameters are stored in P's stack space because they are addressed from% rsp upwards (+ 8n).

3.2 Use stack memory to save function local data

The number of registers is limited, and is divided into two categories: caller save and callee save. P During execution, if there is too much local data, it must save the data in the callee save register on the stack before using them, and retrieve the original value from the stack before returning.

If after adding the callee save register, the number is still not enough, then it is more necessary to push the data onto the stack.
As mentioned earlier, when you need to access data in an array, you must save the data in memory and then access it with the first address plus the offset, instead of using registers that are not related to each other. At this time, we can use push to save the elements of the array to memory one by one, but using push to save data has two drawbacks: each push instruction must have a pop instruction, and the number of pushes must be known in advance.

In order to reduce the consumption of push-pop and support for dynamically specifying the length of the array, the compiler uses the stack frame mode.
Stack frame mode depends on another register% rbp. Its use process is:

  • (push% rbp) Save the original value of% rbp
  • (movq% rsp,% rbp) Use% rbp to save the value of% rsp as the starting point of the stack frame (base-pointer)
  • (subq 8n,% rsp)% rsp minus 8n to open up n addresses for storing local variables
  • (addq 8n,% rsp)% rsp plus 8n to reclaim the allocated n addresses
  • (movq% rbp,% rsp) restore% rsp from% rbp
  • (popq% rbp) Restore the original value of% rbp from% rsp

Of course, although it supports the dynamic allocation of the space required for n elements, n can not be infinitely large, and the specific number can be configured. If it exceeds this size and causes an error, then go to StackOverflow to find out how to solve it.

4. Summary

There are two main problems to be solved in function call:

  • How come
  • How to save function state

The solution to the first problem is:

  • Use the stack pointer that expands and contracts synchronously with the function to save the return address

The solution to the second problem is:

  • Registers are classified into caller save and callee save to maintain the independent state of the function
  • Push the data beyond the number of registers to memory according to the stack pointer and use pop to recover the address and maintain the stack pointer state
  • Save the array state data to the frame, reduce the consumption of push-pop and support the dynamic stack size

Original address: https://www.imhuwq.com/2019/03/10/%E4%BB%8E%E6%B1%87%E7%BC%96%E7%9A%84%E8%A7%92% E5% BA% A6% E7% 90% 86% E8% A7% A3% E7% A8% 8B% E5% BA% 8F% EF% BC% 88% E4% B8% 89% EF% BC% 89% E2% 80% 94% E2% 80% 94% 20% E5% 87% BD% E6% 95% B0% E8% B0% 83% E7% 94% A8 /

Published 8 original articles · Likes4 · Visits 290

Guess you like

Origin blog.csdn.net/qq_45521281/article/details/105408267