Memory management is important in any programming language. On the one hand, memory usage is limited for the operating system, and on the other hand, memory changes can affect the efficiency of our program execution.
We choose to study based on C language because we can use some tools. For example, the use of GDB convenient debugging our program, from each step of the debugging, to see the program running changes.
What can you learn from this episode?
Before we begin, here are a few questions for readers to ponder and some of the things you can learn from this lecture:
- How much memory can our 32-bit operating system manage? What about 64-bit?
- What segments is the memory space divided into, and what does each segment store?
- Why is a stack a contiguous memory space?
- Why does recursion cause stack overflow?
- How to apply heap memory?
Front knowledge
Just a quick list of the basics that we’re going to use.
- The smallest unit of a computer is a byte, which is equal to 8 bits.
- The binary used at the bottom of the computer, if it is used to show that it is usually in base 10, is used in programming in hexadecimal, memory address encoding is used in hexadecimal.
- One hexadecimal digit represents four binary digits.
- In 32-bit operating systems, a pointer occupies 4 bytes, while in 64-bit operating systems, a pointer occupies 8 bytes (in C language, the memory address of pointer variable occupies 8 bytes).
Question: How much memory can our 32-bit operating system manage? What about 64-bit?
The address bus of a 32-bit operating system is 32 bits, which means that the addressing space is 32 bits. Since memory is addressed in bytes, each byte can be interpreted as corresponding to an address number, as shown below, which can be all zeros or all ones.
00000000 00000000 00000000 00000000. . . .11111111 11111111 11111111 11111111
Copy the code
The number of address numbers that a 32-bit operating system can assign isThe permutations and combinations are converted according to the formula:
Ultimately, our 32-bit operating system can manage up to 4 GB of memory.
Note: 1024 byte = 1 KB | 1024 KB = 1 MB | 1024 MB = 1 gb.
* * memory access is much faster than the disk drive, so 4 gb not certainly can’t satisfy the demand, followed by a 64 – bit operating system, in theory, it can manage memory space for 2 64, that number is big, the memory is enough now, we usually is not so much.
Memory division
Memory is managed by the operating system, which numbers our memory and isolates user memory from operating system memory.
On 64-bit operating systems, we can use the first 48 bits, 0x0000000000000000 to 0x00007FFFFFFFFFFF, In kernel mode, the last bit of user mode is 0xFFFF800000000000 ~ 0xFFffFFFFFFFF by 1.
Question: What segments is the memory space divided into, and what does each segment store?
From the figure above, we can clearly see that our memory is divided. One part is the kernel space of the system, and the other part is the user space. The main part related to our program is the user space, which divides the memory into stack, heap, data segment and code segment. Here are the answers.
Code segment
The code segment holds the machine code of our code after it was compiled, and the memory address of the code segment is also the smallest. For example, starting with 0x4, you can remember this value and compare the memory size in other sections later.
(gdb) p &swap
$11 = (void(*) (int *, int *)) 0x40052d <swap>
(gdb) p &main
$12 = (int(*) ())0x400559 <main>
Copy the code
Data segment
The data section holds static variables, constants, and some global variables. Here is an example of two functions that define the static variable count and execute the global variable globalCount.
#include <stdio.h>
int globalCount = 0;
int add(int x, int y) {
static int count = 0;
count++;
globalCount++;
return x + y;
}
int sub(int x, int y) {
static int count = 0;
count++;
globalCount++;
return x + y;
}
int main(a) {
int a = 6;
int b = 3;
int s1 = add(a, b);
int s2 = sub(a, b);
printf("s1=%d s2=%d", s1, s2);
}
Copy the code
The add function prints the memory addresses of count and globalCount, respectively.
The static variable count is declared inside the function, so the two printed addresses are different. Naturally, the two will not affect each other. The global variable can see that the memory address is the same, so the value will change if you change it in either function.
0x601038, 0x60103c, and 0x601040 are incremented by 4 bytes at a time. You can see that the memory address of the data segment is successively incremented by 4 bytes at a time. The memory address of the data segment starting with 0x6 is larger than that of the code segment.
add (x=6, y=3) at main2.c:5
5 count++;
(gdb) p &count
$1 = (int *) 0x60103c <count2181.>
(gdb) p &globalCount
$2 = (int *) 0x601038 <globalCount>
sub (x=6, y=3) at main2.c:11
11 count++;
(gdb) p &count
$3 = (int *) 0x601040 <count2186.>
(gdb) p &globalCount
$4 = (int *) 0x601038 <globalCount>
Copy the code
Another type of data segment is called “BSS” segment, which represents all global or static variables that are not initialized or initialized to 0. Static int A or global int A is called “uninitialized data segment”.
About the initialization data segment with the uninitialized data section, there are articles is also very good, can reference zhuanlan.zhihu.com/p/62208277.
The stack segment
The stack register segment, which points to the segment containing the current program stack, is temporary information. For example, local variables, function parameters, return values, and current program status are all stored in the stack, and can also be popped off the stack after the corresponding scope of these temporary variables is completed.
An example of a variable exchange
The following is an example of C code that uses the swap function to exchange two variables.
// main.c
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(a) {
int a = 2;
int b = 3;
swap(&a, &b);
printf("a=%d b=%d", a, b);
}
Copy the code
GCC -g main.c -o main.out GCC -g main.c -o main.out GCC -g main.c -o main.out
Answer: Why is a stack a contiguous memory space?
In C, the size of an integer is 4 bytes. The memory address of the integer variable A is 0x7ffffFFFE35c, or the beginning address. The value is 0x7FFFFFFFE35C, 0x7FFFFFFFE35D, 0x7FFFFFFFE35E, or 0x7FFFFFE35F based on 4 bytes. The memory address of integer variable B is 0x7FFffFFFE358, 0x7FFffFFFE358, 0x7FFFFFE359, 0x7FFFFFE35a, 0x7FFFFFE35b (plus 4) The number of bytes is immediately adjacent to the variable A, so we can also confirm that the stack is a contiguous area of memory.
Just to give you a little bit of intuition.
In this case, a question may arise, why create variables in order A, B and allocate memory addresses in decreasing order?
This involves storage structure, the stack is advanced after the address on the top of the stack is preset by the system, by the stack into the stack, then the way each memory address will decrease, in turn, allocation, when there are new elements will continue to pressure the stack, the first into the stack at the end of the stack, can also be understood as the bottom of the stack high corresponding address corresponding to lower address, and stack.
(gdb) p &a
$1 = (int *) 0x7fffffffe35c
(gdb) p &b
$2 = (int *) 0x7fffffffe358
Copy the code
Use GDB to debug the swap function. These two parameters a and B are defined as pointer types. You can see that their values are the memory addresses of the outer integer variables A and B.
Pointer variables A and B in swap also have memory addresses, which can be printed to see. Again, the difference between these two memory addresses is 8 bytes, so it fits the definition of a pointer type. On a 64-bit system, a pointer takes 8 bytes. Of course, you might read in your college textbooks that a pointer takes 4 bytes, and that’s for a 32-bit system.
swap (a=0x7fffffffe35c, b=0x7fffffffe358) at main.c:3
3 int temp = *a;
(gdb) p &a
$1 = (int* *)0x7fffffffe328
(gdb) p &b
$2 = (int* *)0x7fffffffe320
Copy the code
At line 3 of this code, the pointer variable A in swap stores the memory address of variable A passed in from the outer layer. How to obtain this value? In C, the operator * can be used to obtain the value of a memory address, that is, “dereference”.
(gdb) p *a
$3 = 2
Copy the code
The program stops at line 5 and you can see that the value of A has changed from 2 to 3. The reason why swap can swap two variables is because we changed the memory address of the two variables passed in by pointer here.
(gdb) n
4 *a = *b;
(gdb) n
5 *b = temp;
(gdb) p *a
$4 = 3
Copy the code
Look at the function stack
Through BT can print all the information of the current function call stack, there is a #0, #1 serial number on the left, 0 is the current top of the stack, because our program is very simple, the program entry function main() is our bottom of the stack, and the current implementation of the swap() function is our top of the stack, but also the current program location.
(gdb) bt
#0 swap (a=0x7fffffffe35c, b=0x7fffffffe358) at main.c:5
#1 0x0000000000400582 in main (a) at main.c:11
Copy the code
Stack overflow
The stack size is 8192 # stack size (kbytes). The default stack size for Linux users is 8MB.
Stack overflow due to recursion
When writing recursions, you usually want to control the boundaries, avoid infinite recursion, keep the recursion level low, and try not to define too much data on the stack. A recursively called program looks like this:
#include <stdio.h>
void call(a)
{
int a[2048];
printf("hello call! \n");
call();
}
int main(int argc, char *argv[]) {
call();
}
Copy the code
The following error message is displayed after GDB debugging:
Program received signal SIGSEGV, Segmentation fault.
0x000000000040053d in call (a) at a.c:6
Copy the code
Bt-n prints n messages from the bottom of the stack, the bottom of which is our main function. In addition, we can see that call() is recursively called 1022 times, because the top sequence number starts at 0.
(gdb) bt - 5
#1018 0x000000000040054c in call (a) at a.c:7
#1019 0x000000000040054c in call (a) at a.c:7
#1020 0x000000000040054c in call (a) at a.c:7
#1021 0x000000000040054c in call (a) at a.c:7
#1022 0x0000000000400567 in main (argc=1, argv=0x7fffffffe458) at a.c:10
Copy the code
Answer to the question: Why does recursion cause stack overflow?
When we a recursive function, at this time of every recursive stack operation, the operation can do pressure after the stack is an advanced data structure, the system is also the largest space limitation, Linux users under the default stack size is 8 MB, when stack storage capacity exceeds the limit, our program will usually get a stack overflow error.
Leave a problem we think about 🤔 : through the above we know that the recursion level is too deep will lead to stack overflow, this is because the system will have stack space size limit, the author usually use JavaScript to meet more, if it is in JavaScript encountered this problem how to solve? If you don’t know, I’m currently writing a series of articles called The JavaScript Asynchronous Programming Guide that will take you through this.
Stack overflow caused by character array
It’s easy to simulate the problem by creating an oversized character array.
#include<stdio.h>
int main(a)
{
char str[8192 * 1024];
int size = sizeof(str);
printf("size: %d\n", size);
}
Copy the code
GDB debugging results in a “Segmentation fault”, which is also called a segment error. A Segmentation fault refers to a memory that exceeds the memory space set by the system for the program, including non-existent memory addresses, memory addresses that access system protection, memory addresses that access read-only memory, and stack overflow.
Program received signal SIGSEGV, Segmentation fault.
0x000000000040054e in main (a) at index.c:7
Copy the code
To solve this problem, read on
Heap segment
Heap segments are allocated and released manually by the developer, also known as dynamic memory allocation. In C language, you can use the functions malloc() and free() provided by the system to apply and free memory.
Continuing with the char array stack overflow example above, we now create memory in the heap. In this case, only the address of the pointer variable STR is stored in the stack, and the real data is stored in the heap, so there is no stack overflow problem.
#include <stdio.h>
#include <malloc.h>
int main(a)
{
char *str = (char *)malloc(8192 * 1024);
if (str == NULL) {
printf("Heap memory application failed.");
return 0;
}
printf("Heap memory application successed.");
free(str);
str = NULL;
return 0;
}
Copy the code
When you go into GDB debugging, the code stops at line 5, and before you allocate heap memory, you can print STR and see that it has no value, whereas & STR takes the variable’s memory address 0x7ffFFFFFE368 in stack space, which is not the same thing, this is the value of the variable.
Free (STR) prints STR to get 0x7ffFF720C010 at which point the heap has been allocated successfully.
Now let’s do the release, stopping at line 14, and print STR to see that the value has been released.
# Temporary breakpoint not allocated1, main () at b.c:5
5 char *str = (char *)malloc(8192 * 1024);
(gdb) p str
$1 = 0x0
(gdb) p &str
$2 = (char* *)0x7fffffffe368N # allocated6 if (str == NULL) {
(gdb) n
10 printf("Heap memory application successed."); (GDB) n # release12 free(str);
(gdb) p str
$3 = 0x7ffff720c010 ""
(gdb) n
13 str = NULL;
(gdb) n
14 return 0;
(gdb) p str
$4 = 0x0
(gdb) p &str
$5 = (char* *)0x7fffffffe368
Copy the code
conclusion
This article is also the summary of the author in the previous learning process, and has been sorted out a little recently. It is also sent out in the hope of sharing and communication with everyone.
Through this article, several common knowledge points: stack and heap difference, why recursion will cause stack overflow, similar to this common problem, hope readers can master friends.
Finally, welcome to pay attention to the public number [May jun] to learn new knowledge together!
Why does recursion cause stack overflow? Explore the program’s memory management!