Stack management (3/7)

The English name for stack is Stack. Heap and stack are not the same concept. They are two different areas in memory, and their management and maintenance methods are also different.

The stack is a data structure that is first in, last out. There are two basic operations on the stack: push and pop. Pushing is pushing a stack element onto the stack, while popping is popping a stack element from the stack. Pushing and popping from the stack are maintained by the stack pointer, which moves up and down on the top of the stack as the stack is pushed and popped. Depending on the stack pointer SP pointing to the top element of the stack, the stack can be divided into a full stack and an empty stack; according to the growth direction of the stack, the stack can be divided into an increasing stack and a decreasing stack. The types of stacks are different, and the stack pointer operates differently when popping and pushing.

Insert image description here
The stack is the basis for the C language to run. The local variables, passed actual parameters, returned results, and temporary variables generated by the compiler in C language functions are all stored on the stack. Without the stack, the C language cannot run. In the startup code of many embedded systems, you will see that the assembly code that starts running as soon as the system is powered on is initialized before jumping to the first C language function to run.

1. Initialization of the stack

The initialization of the stack is actually the initialization of the stack pointer SP. During the system startup process, after the memory is initialized, the stack pointer points to a space in the memory to complete the initialization of the stack. The memory space pointed by the stack pointer is called the stack space. Different processors generally use special registers to save the starting address of the stack. x86 processors generally use ESP (top pointer of stack) and EBP (bottom pointer of stack) to manage stacks, while ARM processors use R13 register (SP) and R11 register (FP) to manage stacks.

During the initialization process of the stack, the starting address of the stack in memory is still a bit particular. The ARM processor uses a full decrementing stack. In the Linux environment, the starting address of the stack is generally the highest address of the process user space, next to the kernel space, and the stack pointer grows from high address to low address. In order to prevent stack overflow attacks by hackers, new versions of the Linux kernel generally set the starting address of the stack to a random one. Each time the program runs, the initial starting address of the stack will have a random offset based on the highest address of user space. The starting address of the stack is different every time.

Insert image description here

After the stack is initialized, the stack pointer points to the top of the stack space. When push or pop operations are required, the stack pointer SP will move up and down as the top of the stack changes. In a full decreasing stack, the stack pointer SP always points to the top element of the stack.

Insert image description here

During the initialization process of the stack, in addition to specifying the starting address of the stack, we also need to specify the size of the stack space. In the Linux environment, we can view and set the stack size through the following commands:

jiaming@jiaming-pc:~$ ulimit -s
8192 # KB

ulimit -s xxxSet the stack space size.

Linux allocates 8MB of space to each user process by default. If the stack capacity is set too large, it will increase memory overhead and startup time; if it is set too small, the program will exceed the memory space set by the stack and it will easily cause stack overflow and generate a segmentation fault. Local variables defined within a function are stored in the stack space.

When setting the stack size, set a reasonable stack size based on the actual demand for stack space by variables and arrays in the program. In order to prevent stack overflow when writing programs, users can refer to the following principles:

  • Try not to use large arrays within functions. If you really need to use a large block of memory, you can use malloc to apply for dynamic memory.
  • The number of nested levels of functions should not be too deep.
  • The number of levels of recursion should not be too deep.

2. Function call

The stack is the basis for C language operation. The local variables defined within a function and the actual parameters passed are stored in the stack. Each function will have its own dedicated stack space to save this data. The stack space of each function is called a stack (Frame Stack, FP). Each stack frame is maintained using two registers FP and SP. FP points to the bottom of the stack frame and SP points to the top of the stack frame.

In addition to saving local variables and actual parameters, the stack frame of a function is also used to save the context of the function. There are often multiple levels of function calls in a program. Each level of call will run a different function. Each function has its own stack frame space. Each stack frame has a stack bottom and a stack top. No matter where the function call runs, Level, SP always points to the top of the stack frame of the currently running function, and FP always points to the bottom of the stack of the currently running function. In each function stack frame, in addition to saving local variables, function arguments, and the return address of the function caller, sometimes some temporary variables during the compilation process are also saved in the function stack frame . In addition, the starting address of the upper-level function stack frame, that is, the bottom of the stack, will also be saved in the current function call stack. Multiple stack frames form a chain through FP, and this chain is the function call stack of a certain process. Many debuggers support the backtrace function, which actually analyzes the function calling relationship based on this call chain.


```jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat main.c
#include <stdio.h>

int g(void)
{
    
    
	int x = 100;
	int y = 200;
	return 300;
}

int f(void)
{
    
    
	int l = 20;
	int m = 30;
	int n = 40;
	g();
	return 50;
}

int main(void)
{
    
    
	int i = 2;
	int j = 3;
	int k = 4;
	f();

	return 0;
}

jiaming@jiaming-pc:~/Documents/CSDN_Project$ arm-linux-gnueabi-gcc main.c -o a.out
jiaming@jiaming-pc:~/Documents/CSDN_Project$ arm-linux-gnueabi-objdump -D a.out > a.S
jiaming@jiaming-pc:~/Documents/CSDN_Project$ cat a.S
...
000103c8 <g>:
   103c8:	e52db004 	push	{
    
    fp}		; (str fp, [sp, #-4]!)
   103cc:	e28db000 	add	fp, sp, #0
   103d0:	e24dd00c 	sub	sp, sp, #12
   103d4:	e3a03064 	mov	r3, #100	; 0x64
   103d8:	e50b300c 	str	r3, [fp, #-12]	; 将r3的值存入fp 值减12地址处
   103dc:	e3a030c8 	mov	r3, #200	; 0xc8
   103e0:	e50b3008 	str	r3, [fp, #-8]
   103e4:	e3a03f4b 	mov	r3, #300	; 0x12c
   103e8:	e1a00003 	mov	r0, r3
   103ec:	e28bd000 	add	sp, fp, #0
   103f0:	e49db004 	pop	{
    
    fp}		; (ldr fp, [sp], #4)
   103f4:	e12fff1e 	bx	lr

000103f8 <f>:
   103f8:	e92d4800 	push	{
    
    fp, lr}
   103fc:	e28db004 	add	fp, sp, #4
   10400:	e24dd010 	sub	sp, sp, #16 ; sp = sp - 16
   10404:	e3a03014 	mov	r3, #20
   10408:	e50b3010 	str	r3, [fp, #-16]
   1040c:	e3a0301e 	mov	r3, #30
   10410:	e50b300c 	str	r3, [fp, #-12]
   10414:	e3a03028 	mov	r3, #40	; 0x28
   10418:	e50b3008 	str	r3, [fp, #-8]
   1041c:	ebffffe9 	bl	103c8 <g>
   10420:	e3a03032 	mov	r3, #50	; 0x32
   10424:	e1a00003 	mov	r0, r3
   10428:	e24bd004 	sub	sp, fp, #4
   1042c:	e8bd8800 	pop	{
    
    fp, pc}

00010430 <main>:
   10430:	e92d4800 	push	{
    
    fp, lr}
   10434:	e28db004 	add	fp, sp, #4
   10438:	e24dd010 	sub	sp, sp, #16
   1043c:	e3a03002 	mov	r3, #2
   10440:	e50b3010 	str	r3, [fp, #-16]
   10444:	e3a03003 	mov	r3, #3
   10448:	e50b300c 	str	r3, [fp, #-12]
   1044c:	e3a03004 	mov	r3, #4
   10450:	e50b3008 	str	r3, [fp, #-8]
   10454:	ebffffe7 	bl	103f8 <f>
   10458:	e3a03000 	mov	r3, #0
   1045c:	e1a00003 	mov	r0, r3
   10460:	e24bd004 	sub	sp, fp, #4
   10464:	e8bd8800 	pop	{
    
    fp, pc} ; [SP]-->PC, SP=SP+4, [SP]-->FP
...

Why is LR not pushed on the stack in the disassembly code of the g() function?

After the main() function jumps into the f() function, the f() function will first save the return address LR of the main() function and the stack frame base address FP in its own stack frame through a push operation, and wait for f( ) function ends, you can return to the main() function according to LR to continue execution. When the f() function jumps to the g() function, because the BL instruction is not used in the g() function to call other functions, the value of the LR register remains unchanged during the entire running of the g() function. Is the return address of the upper-level function f(). In order to save memory resources and reduce the time and space overhead caused by pushing the stack, LR is not pushed onto the stack. When the g() function ends, assign the return address in the LR register to the PC pointer, and then you can directly return to the previous level f() function to continue running.

3. Parameter passing

After analyzing the storage of the function's local variables and return values ​​in the stack, we next analyze the activity records in the stack of actual parameters passed between functions during the function call process.

Parameter transfer during function calling is generally done through the stack. In order to improve program running efficiency, the ARM processor will use registers to transfer parameters. According to ATPCS rules, during the function call process, when the parameters to be transferred are less than 4, just use the R0~R3 registers to transfer them directly; when the parameters to be transferred are When the number is greater than 4, the first 4 parameters are transferred using registers, and the remaining parameters are pushed onto the stack for storage.

The order in which parameters are pushed onto the stack is determined by common call management:

Insert image description here
The C language uses the cdecl calling convention by default. When passing parameters, they are pushed onto the stack in order from right to left. The cleanup method of the stack is managed by the function caller. The advantage of using cdecl call management is that the parameters and return value sizes can be known in advance, and it can support the call of variable parameter functions, such as the printf() function.

4. Formal participation in actual parameters

The parameters of a function are passed by value. The formal parameters save a copy of the actual parameters. Changing the formal parameters will not change the actual parameters.

Insert image description here
In addition to FP and LR, the six local variables h, i, j, k, l, and m defined in the main function will be stored in the function stack frame respectively. These six local variables are passed to the f() function as actual parameters: the first four actual parameters 1, 2, 3, and 4 are passed through the registers R0 ~ R3, and the last two actual parameters 5 and 6 are passed through the stack. Before jumping into the f() function, the passed actual parameters 5 and 6 are pushed into the stack frame of the main() function. Then in the f() function, change the value of the passed actual parameter m from the original 6 to 100. This actual parameter value is stored in the memory address of the formal parameter ag6. The formal parameter variable ag6 is used to save the passed in actual parameter. Although the value was modified in the f() function, in the main() function we can see that the value of m has not changed, and the value of m is still 6.

Insert image description here
Through the above actual code analysis, it can be concluded that the formal parameters will only allocate temporary storage units on the stack when the function is called to save the passed actual parameter values. When a variable is passed as an actual parameter, the variable value is simply copied to the formal parameter. The formal parameter and the actual parameter are located in different storage units on the stack. After understanding the storage of formal parameters and actual parameters on the stack, we also understand why the value of the actual parameters does not change when the formal parameters are changed.

Insert image description here
Insert image description here

The actual parameters passed to it through the main() function are actually copies of i and j, which are stored in different storage units on the stack. In the stack frame of the swap() function, no matter how we modify the formal parameter variables a and b, whether they are exchanged or reassigned, the values ​​of the variables i and j in the stack frame of the main() function will not be changed.

Formal parameters only allocate storage units within the function frame when the function is called to receive the passed in actual parameter values. After the function ends, the formal parameter unit is released as the stack frame is destroyed. When a variable is passed as an actual parameter, its value is simply copied to the storage space of the formal parameter. During the running of the function, changing the value of the formal parameter will not change the value of the original actual parameter, because the two are stored in different memory units on the stack. superior. By understanding the dynamic changes of formal parameters in the stack, you can better understand the life cycle and scope of local variables.

5. Stack and Scope

Scope of variables: Global variables are defined outside the function, and their scope ranges from the declaration to the end of the file. If other files want to use this global variable, they can use it after using extern declaration in their own file. The declaration cycle of global variables is valid throughout the entire program runtime.

Local variables are defined within a function, and their scope can only be used within the function body. A function will open a stack frame space in the memory only when it is called, and store local variables and passed function parameters in this stack space. When the function call ends, the stack frame space is destroyed and released, and the variables disappear accordingly. Therefore, the life cycle of local variables only exists during the running of the function. Each time a function is called, the temporarily allocated stack frame space may be different, and the addresses of local variables may also be different.

After understanding the activity record and life cycle of local variables during the function call and within the function stack frame, we also understand why the scope of local variables is limited to the function, and why we cannot access local variables in other functions. When the compiler compiles a program, it actually limits the scope of a variable based on a pair of braces.

Summary of variable scope:

The scope of global variables is as follows:

  • The scope of global variables is limited by the file.
  • It can be expanded using extern and referenced by other files.
  • You can also use static to restrict it and can only be referenced in this file.

The scope of local variables is as follows:

  • The scope of local variables is delimited by curly braces.
  • You can use static to modify local variables to change their storage properties (lifecycle), but not their scope. Define a local variable in the function and modify it with static parameters. Its storage location is transferred from the stack to the data segment, but the scope is still limited to the code block delimited by curly braces.

6. Principle of stack overflow attack

The stack space of a Linux process has a fixed size, usually 8MB. If an array is defined within a function, the system will allocate storage space for the array on the stack. Due to the laxity of bounds checking in C language, even if the program tamper with data in memory units beyond the array, the compiler will generally not report an error.

In addition to the beauty of simplicity, the philosophy of C language also has another feature: the laxity of syntax checking, which assumes that all programmers are masters and will never make mistakes when operating memory. However, it is this flexibility of programming that gives hackers an opportunity. They can take advantage of the looseness of the C language's syntax check, use stack overflow to implant their own instruction codes, seize control of the program, and then conduct malicious attacks.

In the stack frame of a function, the return address of the upper-level function is generally saved. When the function ends, it will jump to the upper-level function based on this return address to continue execution. If a hacker finds a loophole in a function you implement, he can use the loophole to modify the return address LR of the stack and implant his own instruction code.

// virus.c
void shellcode(void)
{
    
    
	printf("virus run success!\n");
	// do something you want
	while(1);
}

void f(void)
{
    
    
	int a[4];
	a[8] = shellcode;
}

int main(void)
{
    
    
	f();
	printf("hello world!\n");
	return 0;
}

In the stack overflow program above, the main() function calls the f() function. Under normal circumstances, f() will return to main() to continue execution after running. However, due to the out-of-bounds array access in the f() function, the stack frame structure of the f() function is destroyed: the return address of the main() function in the f() function stack frame is overwritten and replaced with its own virus code shellcode. entrance address. So when the f() function finishes running, it does not return to main(), but jumps to shellcode() for execution. Due to the laxity of boundary checking in C language, the compiler does not report an error when accessing array element a[8] in the program. The hacker used the overflow of the array to seize control of the program, and the attack was successful.

Insert image description here

Although the C language standard does not stipulate that out-of-bounds access to an array will report an error, most compilers will check the boundaries of the array themselves for safety reasons: when an array out-of-bounds access is found, an error message will be generated to remind the developer.

In order to prevent array out-of-bounds access, the GCC compiler generally places a protection variable at the end of a user-defined array, and determines whether the array is accessed out-of-bounds based on whether this variable has been modified. If it is found that the value of this variable has been overwritten, a SIGABRT signal will be sent to the current process and the current process will be terminated. This detection method is simple and effective, but it also has loopholes: if the user bypasses this monitoring point, GCC may not be able to detect it.

Guess you like

Origin blog.csdn.net/weixin_39541632/article/details/132549860