我们知道一段程序要在计算机中运行,他需要占用内存,一部分用来存储程序内容,一部分用来运行程序(程序运行过程中也是需要申请内存的)。忽略部分细节,一段程序加载到linux内存后大概是这样存储的。
内核空间 | 内核空间
1G |
用户空间
3G | |
栈 | |
内存映射区 | |
堆区 | |
bss段 | |
数据段 | |
代码段 | |
保留区 |
32位系统虚拟内存分布
从图上我们看到linux将内存分成两部分:内核空间和用户空间。用户态的程序在用户空间执行,内核态的程序在内核空间执行,自己跑自己的,互不影响。
当然,即使简单如hello world这样的程序也需要同时使用到用户空间和内核空间,关键在于printf函数,在用户看来只是一个库函数,其实他需要内核去调用驱动程序输出到屏幕上,内核的这部分工作就是在内核空间完成的。
我们可以看到用户空间和内核之间有一段灰色内容,操作系统向里面填写了随机值,当值被改变时就会终止用户程序,防止内核空间内容被篡改,这是系统的自我保护机制。当然你更不能在用户态直接去访问内核空间的地址!比如我们遇到很多的段错误就是因为这个原因。
其实,我们可以把操作系统比喻成一个渣男,他有4G的物理内存(实际的爱心上限),但是他告诉每个用户程序(单纯女孩)我只爱一个,我把所有的内存(爱)都给你。而且对于每个女孩来说,在操作系统完美的进程调度下,她们时感知不到他同时在跟多个妹子暧昧,操作系统的时间管理不是一般强啊!这种将物理内存同时映射到多个用户进程的技术叫虚拟内存,也就是上图所示的内容。
至于操作系统怎么做到的,这里简单举个例子,深入理解可以去看看《深入理解计算机系统》这本书。
假设你的计算机是32位,物理内存是1G,硬盘是20G。那么理论上你的地址总线可以访问0~4G(2的32次方),操作系统给进程A和进程B的虚拟内存也都是4G,首先操作系统创建一个页表,页是访问内存的最基本单位。比如这个页表范围是0~1023,即一个页表项映射4M的物理空间,什么?不够用?操作系统并没有很老实的把这0~1023都填上,而是进程申请了才会把对应的页表标记成已经使用!
当物理内存映射完后,系统就会去硬盘上映射(swap空间),当然这个时候访问就会慢很多,机器会变得卡顿。
那当页表里的都用完了呢?操作系统会使用页置换算法—LRU算法,将不常用的内存和硬盘中的调换!将不长用的换到硬盘中去,常用的标记到页表中来!
其实这也是利用了程序的局部性原理,一个程序经常使用的内存和逻辑肯定只是部分。
32位系统虚拟内存分布
虚拟内存分布在上图中自上而下,从到高地址到低地址:0xFFFFFFFF ~ 0x00000000,
即:内核空间:0xFFFFFFFF ~ 0xC0000000 1G,用户空间:0x00000000 ~ 0xBFFFFFFF 3G.
64位系统虚拟内存分布
虚拟内存分布在上图中自上而下,从到高地址到低地址:0xFFFFFFFFFFFFFFFF ~ 0x0000000000000000
即:内核空间:0xFFFFFFFFFFFFFFFF ~ 0xFFFF000000000000 256T,用户空间:0x0000000000000000 ~ 0x0000FFFFFFFFFFFF 256T.
内核空间和用户空间的大小是可以在操作系统中配置的,不是写死的。
具体使用的区别可以参考下面的图:
从图可以看出64位有一半的内存时空着的,毕竟已经足够大了。
但是无论是32位还是64位各自区域用来存储的内容都是相同。
栈区:局部变量,函数参数,返回地址
映射区:动态库
堆区:动态分配的内存
bss段:初始化为0或者未初始化的全局变量和局部变量
数据段:初始化的全局变量和静态局部变量
代码段:可执行代码,字符串
下面用64位系统做个实验验证一下:
#include <stdio.h>
#include <stdlib.h>
int global_1 = 10; //初始化的全局变量
int global_2 = 0; //初始化为1的全局变量
int global_3; //未初始化的全局变量
int main()
{
static int static_global = 10; //静态局部变量
char *str = "12345"; //文本字符串
int stack[10]; //局部变量
int *heap = (int*)malloc(sizeof(int)*10); //动态分布的空间
int const const_value = 99; //只读变量
printf("address:\n");
printf("global_1 %p\n", &global_1);
printf("global_2 %p\n", &global_2);
printf("global_3 %p\n", &global_3);
printf("static_global %p\n", &static_global);
printf("str %p\n", str);
printf("stack %p\n", stack);
printf("heap %p\n", heap);
printf("const_value %p\n", &const_value);
printf("main %p\n",main);
return 0;
}
运行结果:
[root@VM-0-2-centos virmem]# ./a.out
address:
global_1 0x601044
global_2 0x601050
global_3 0x601054
static_global 0x601048
str 0x400740
stack 0x7fffd87e71c0
heap 0x1f89010
const_value 0x7fffd87e71bc
main 0x4005bd
可以看到:
栈自高地址向下分配:stack 0x7fffd87e71c0
堆地址自下而上分配:heap 0x1f89010
未初始化(初始化为0)全局:global_2 0x601050
初始化(静态局部):static_global 0x601048
代码段,字符串:main 0x4005bd str 0x400740
函数栈溢出
程序的运行无非就是一群函数,你调用我,我调用你,这个调用过程是在栈中完成的,把一个函数在栈中分配空间成为函数栈帧。
看下面的例子,我们无限递归调用,会发生什么事情?
#include <stdio.h>
void fun()
{
static unsigned int i = 0;
i++;
printf("i = %u\n", i);
fun();
}
int main()
{
fun();
return 0;
}
输出结果:
i = 523989
i = 523990
i = 523991
i = 523992
i = 523993
i = 523994
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a8725e in _IO_new_file_write () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-307.el7.1.x86_64
(gdb) bt
#0 0x00007ffff7a8725e in _IO_new_file_write () from /lib64/libc.so.6
#1 0x00007ffff7a88a7e in __GI__IO_do_write () from /lib64/libc.so.6
#2 0x00007ffff7a879c0 in __GI__IO_file_xsputn () from /lib64/libc.so.6
调用到523895次时发生了溢出,实际linux中的栈大小并没有前面说的那么大
查看系统设置的栈大小:
[root@VM-0-2-centos ~]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7269
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 100001
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192 =>栈大小8M
cpu time (seconds, -t) unlimited
max user processes (-u) 7269
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
仅仅8M而已,当然可以配置,改成10M
[root@VM-0-2-centos ~]# ulimit -s 10240
[root@VM-0-2-centos ~]# ulimit -s
10240
再跑一下
i = 655061
i = 655062
i = 655063
i = 655064
i = 655065
i = 655066
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a8725e in _IO_new_file_write () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-307.el7.1.x86_64
(gdb) bt
#0 0x00007ffff7a8725e in _IO_new_file_write () from /lib64/libc.so.6
#1 0x00007ffff7a88a7e in __GI__IO_do_write () from /lib64/libc.so.6
#2 0x00007ffff7a879c0 in __GI__IO_file_xsputn () from /lib64/libc.so.6
次数增加了,如果函数中有局部变量或者参数调用次数会更少。
函数因为调用完要返回,所以必须在栈中记录调用者的返回地址,以便调用完成后返回。
函数调用过程
#include <stdio.h>
int say_hello(char *name)
{
printf("helloc %s\n", name);
return 0;
}
int main()
{
char name[10]= "sanme";
say_hello(name);
return 0;
}
使用gdb查看两个函数的汇编:
/* rbp寄存器函数栈基地址 */
/* rsp寄存器用来保存函数栈顶地址 */
/* rdi寄存器用来保存函数参数 */
/* eax寄存器用来保存函数返回值 */
/* rip函数当前运行地址 */
(gdb) disass main
Dump of assembler code for function main:
0x0000000000400556 <+0>: push %rbp //rbp 入栈 等价于 sub $8 %rsp movq %rbp (%rsp)
0x0000000000400557 <+1>: mov %rsp,%rbp //将rsp拷贝到rbp
0x000000000040055a <+4>: sub $0x10,%rsp //将rsp寄存器移动到减0x10位置,相当于给name变量开辟栈空间
0x000000000040055e <+8>: movabs $0x656d6e6173,%rax //将“sanme”复制到rax中,字符串“emnas”的十六进制0x656d6e6173,这里跟系统大小端有关系
0x0000000000400568 <+18>: mov %rax,-0x10(%rbp) //这几句将“sanme”复制到开辟的栈内存中
0x000000000040056c <+22>: movw $0x0,-0x8(%rbp)
0x0000000000400572 <+28>: lea -0x10(%rbp),%rax
0x0000000000400576 <+32>: mov %rax,%rdi //将name变量地址拷贝到rdi寄存器
0x0000000000400579 <+35>: callq 0x40052d <say_hello> //调用say_hello函数,并将0x000000000040057e保存作为say_hello的返回地址
0x000000000040057e <+40>: mov $0x0,%eax //将eax置0
0x0000000000400583 <+45>: leaveq //等价于mov %rbp %rsp,准备结束了,指向函数开头rbp
0x0000000000400584 <+46>: retq //等价于popq %rbp
End of assembler dump.
(gdb) disass say_hello
Dump of assembler code for function say_hello:
0x000000000040052d <+0>: push %rbp
0x000000000040052e <+1>: mov %rsp,%rbp
0x0000000000400531 <+4>: sub $0x10,%rsp
0x0000000000400535 <+8>: mov %rdi,-0x8(%rbp)
0x0000000000400539 <+12>: mov -0x8(%rbp),%rax
0x000000000040053d <+16>: mov %rax,%rsi
0x0000000000400540 <+19>: mov $0x400620,%edi
0x0000000000400545 <+24>: mov $0x0,%eax
0x000000000040054a <+29>: callq 0x400410 <printf@plt>
0x000000000040054f <+34>: mov $0x0,%eax
0x0000000000400554 <+39>: leaveq
0x0000000000400555 <+40>: retq
End of assembler dump.
注意汇编前面的地址< 0x0000000000400XXXXX>不是栈地址,而是代码段的地址,在程序加载到内存中时,每个函数在代码段中的地址已经分配好了。这样程序就可以按照既定的地址调用每个函数了。
(gdb) b amin
Function "amin" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n
(gdb) b main
Breakpoint 1 at 0x40055e: file fun_call.c, line 10.
(gdb) r
Starting program: /luogf/virmem/./a.out
Breakpoint 1, main () at fun_call.c:10
10 char name[10]= "sanme";
Missing separate debuginfos, use: debuginfo-install glibc-2.17-307.el7.1.x86_64
(gdb) disassemble
Dump of assembler code for function main:
0x0000000000400556 <+0>: push %rbp
0x0000000000400557 <+1>: mov %rsp,%rbp
0x000000000040055a <+4>: sub $0x10,%rsp
=> 0x000000000040055e <+8>: movabs $0x656d6e6173,%rax
0x0000000000400568 <+18>: mov %rax,-0x10(%rbp)
0x000000000040056c <+22>: movw $0x0,-0x8(%rbp)
0x0000000000400572 <+28>: lea -0x10(%rbp),%rax
0x0000000000400576 <+32>: mov %rax,%rdi
0x0000000000400579 <+35>: callq 0x40052d <say_hello>
0x000000000040057e <+40>: mov $0x0,%eax
0x0000000000400583 <+45>: leaveq
0x0000000000400584 <+46>: retq
End of assembler dump.
(gdb) i r
rax 0x400556 4195670
rbx 0x0 0
rcx 0x400590 4195728
rdx 0x7fffffffe638 140737488348728
rsi 0x7fffffffe628 140737488348712
rdi 0x1 1
rbp 0x7fffffffe540 0x7fffffffe540
rsp 0x7fffffffe530 0x7fffffffe530
r8 0x7ffff7dd5e80 140737351868032
r9 0x0 0
r10 0x7fffffffd9e0 140737488345568
r11 0x7ffff7a2f460 140737348039776
r12 0x400440 4195392
r13 0x7fffffffe620 140737488348704
r14 0x0 0
r15 0x0 0
rip 0x40055e 0x40055e <main+8>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
可以使用gdb不断下一步跟踪这几个寄存器的值来观察程序的运行。
整个say_hello调用过程是这样的:
1.参数name入栈
2.返回地址 0x000000000040057e 入栈
3.say_hello函数的局部变量入栈(这里没有)
4.调用printf
就是这样一层套一层,具体的实现过可参考这篇文章,讲的很细https://www.debugger.wiki/article/html/1556499600981380
关于函数参数
X86 64位系统中有6个寄存器用来存储参数,当函数参数大于6个时,就要在栈中开辟空间来存储(数组一般使用栈空间)。而且参数入栈的顺序跟参数传入的顺序相反。