08C语言高级篇之内存精讲

C语言高级篇之内存精讲

1.一个程序在计算机中到底是如何运行的?

img

总结:

内存用于存放指令和数据,不负责计算,CPU是负责计算的,为了节省时间,可以从缓存区直接读取数据,加快速度。

寄存器,用来完成数学运算、控制循环次数、控制程序的执行流程、标记CPU运行状态,速度非常快,Android Darlk就是基于寄存器的。

https://blog.csdn.net/czg13548930186/article/details/52629628?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162312274616780274137223%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162312274616780274137223&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-52629628.first_rank_v2_pc_rank_v29&utm_term=%E4%B8%80%E4%B8%AA%E7%A8%8B%E5%BA%8F%E5%9C%A8%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%AD%E5%88%B0%E5%BA%95%E6%98%AF%E5%A6%82%E4%BD%95%E8%BF%90%E8%A1%8C%E7%9A%84%EF%BC%9F&spm=1018.2226.3001.4187

2.虚拟内存到底是什么?为什么我们在C语言中看到的地址是假的?

总结:

img

问题1:程序A和程序B同时修改了一个内存地址,那么程序AB都会受到影响码?

答:不会,程序A和程序B同一个内存地址,但是只是虚拟地址,真实的物理地址需要过CPU的转换才能对应到物理地址。

问题2:这样做优势?

答:1.使不同程序之间隔离,即使内存地址(虚拟地址)一样,映射之后,物理地址绝对不一样,达到隔离的思想,如同Java中Threadlocals

​ 2.能更好管理内存,提高内存使用效率

​ 3.中间层思想,让上层开发更加容易,而且安全问题更少

https://blog.csdn.net/czg13548930186/article/details/52629639?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162312302916780262544638%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162312302916780262544638&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-1-52629639.first_rank_v2_pc_rank_v29&utm_term=%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E5%9C%A8C%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9C%8B%E5%88%B0%E7%9A%84%E5%9C%B0%E5%9D%80%E6%98%AF%E5%81%87%E7%9A%84%EF%BC%9F&spm=1018.2226.3001.4187

3.虚拟地址空间以及编译模式

我们通常所说的多少位的CPU,除了可以理解为寄存器的位数,也可以理解数据总线的宽度,通常情况下它们是相等的

数据总线决定了CPU单次的数据处理能力,主频决定了CPU单位时间内的数据处理次数,它们的乘积就是CPU单位时间内的数据处理量

4.内存分页机制,完成虚拟地址的映射

img

既然内存是分页的,只要我们能够定位到数据所在的页,以及它在页内的偏移(也就是距离页开头的字节数),就能够转换为物理地址。例如,一个 int 类型的值保存在第 12 页,页内偏移为 240,那么对应的物理地址就是 2^12 * 12 + 240 = 49392。

2^12 为一个页的大小,也就是4K。

虚拟地址空间大小为 4GB,总共包含 2^32 / 2^12 = 2^20 = 1K * 1K = 1M = 1048576 个页面,我们可以定义一个这样的数组:它包含 2^20 = 1M 个元素,每个元素的值为页面编号(也就是位于第几个页面),长度为4字节,整个数组共占用4MB的内存空间。这样的数组就称为页表(Page Table),它记录了地址空间中所有页的编号。

虚拟地址长度为32位,我们不妨进行一下切割,将高20位作为页表数组的下标,低12位作为页内偏移。如下图所示:

img

5.MMU部件以及对内存权限的控制

1.MMU

在CPU内部,有一个部件叫做MMU(Memory Management Unit,内存管理单元),由它来负责将虚拟地址映射为物理地址,如下图所示:

img

在页映射模式下,CPU 发出的是虚拟地址,也就是我们在程序中看到的地址,这个地址会先交给 MMU,经过 MMU 转换以后才能变成了物理地址。

即便是这样,MMU也要访问好几次内存,性能依然堪忧,所以在MMU内部又增加了一个缓存,专门用来存储页目录和页表。MMU内部的缓存有限,当页表过大时,也只能将部分常用页表加载到缓存,但这已经足够了,因为经过算法的巧妙设计,可以将缓存的命中率提高到 90%,剩下的10%的情况无法命中,再去物理内存中加载页表。

有了硬件的直接支持,使用虚拟地址和使用物理地址相比,损失的性能已经很小,在可接受的范围内。

MMU 只是通过页表来完成虚拟地址到物理地址的映射,但不会构建页表,构建页表是操作系统的任务。在程序加载到内存以及程序运行过程中,操作系统会不断更新程序对应的页表,并将页目录的物理地址保存到 CR3 寄存器。MMU 向缓存中加载页表时,会根据 CR3 寄存器找到页目录,再找到页表,最终通过软件和硬件的结合来完成内存映射。

CR3 是CPU内部的一个寄存器,专门用来保存页目录的物理地址。

每个程序在运行时都有自己的一套页表,切换程序时,只要改变 CR3 寄存器的值就能够切换到对应的页表。

2.对内存权限的控制

MMU 除了能够完成虚拟地址到物理地址的映射,还能够对内存权限进行控制。在页表数组中,若每个元素占用4个字节,也即32位,我们使用高20位来表示物理页编号,还剩下低12位,这12位就用来对内存进行控制,例如,是映射到物理内存还是映射到磁盘,程序有没有访问权限,当前页面有没有执行权限等。

操作系统在构建页表时将内存权限定义好,当MMU对虚拟地址进行映射时,首先检查低12位,看当前程序是否有权限使用,如果有,就完成映射,如果没有,就产生一个异常,并交给操作系统处理。操作系统在处理这种内存错误时一般比较粗暴,会直接终止程序的执行。

原文链接:https://blog.csdn.net/Neutionwei/article/details/109605411

6.Linux下C语言程序的内存布局(内存模型)

1.内核空间和用户空间

对于32位环境,理论上程序可以拥有 4GB 的虚拟地址空间,我们在C语言中使用到的变量、函数、字符串等都会对应内存中的一块区域。

但是,在这 4GB 的地址空间中,要拿出一部分给操作系统内核使用,应用程序无法直接访问这一段内存,这一部分内存地址被称为内核空间(Kernel Space)。

Windows 在默认情况下会将高地址的 2GB 空间分配给内核(也可以配置为1GB),而 Linux 默认情况下会将高地址的 1GB 空间分配给内核。也就是说,应用程序只能使用剩下的 2GB 或 3GB 的地址空间,称为用户空间(User Space)。

2.Linux下32位环境的用户空间内存分布情况

img
在这些内存分区中(暂时不讨论动态链接库),程序代码区用来保存指令,常量区、全局数据区、堆、栈都用来保存数据。对内存的研究,重点是对数据分区的研究。

程序代码区、常量区、全局数据区在程序加载到内存后就分配好了,并且在程序运行期间一直存在,不能销毁也不能增加(大小已被固定),只能等到程序运行结束后由操作系统收回,所以全局变量、字符串常量等在程序的任何地方都能访问,因为它们的内存一直都在。

常量区和全局数据区有时也被合称为静态数据区,意思是这段内存专门用来保存数据,在程序运行期间一直存在。

函数被调用时,会将参数、局部变量、返回地址等与函数相关的信息压入栈中,函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了。

常量区、全局数据区、栈上的内存由系统自动分配和释放,不能由程序员控制。程序员唯一能控制的内存区域就是堆(Heap):它是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分,在这片空间中,程序可以申请一块内存,并自由地使用(放入任何数据)。堆内存在程序主动释放之前会一直存在,不随函数的结束而失效。在函数内部产生的数据只要放到堆中,就可以在函数外部使用。

总结:

栈:栈上的内存由系统自动分配和释放,不能由程序员控制。存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了。

堆:一般由程序员分配和释放,若程序员不释放,程序运行结束时由操作系统回收。malloc()、calloc()、free() 等函数操作的就是这块内存。在函数内部产生的数据只要放到堆中,就可以在函数外部使用。

静态数据区常量区和全局数据区意思是这段内存专门用来保存数据,在程序运行期间一直存在。


#include <stdio.h>
#include <stdlib.h>

char *str1 = "blog.csdn.net"; //字符串在常量区,str1在全局数据区
int n;                        //全局数据区

char *func()
{
    char *str = "neutionwei"; //字符串在常量区,str在栈区,
    return str;
}

int main()
{
    int a;                  //栈区
    char *str2 = "01234";   //字符串在常量区,str2在栈区
    char arr[20] = "56789"; //字符串和arr都在栈区
    char *pstr = func();    //栈区neutionwei
    printf("%s\n", pstr);//pstr获取的数据是不对的,因为str在栈区,
    int b; //栈区
    printf("str1: %#X\npstr: %#X\nstr2: %#X\n", str1, pstr, str2);
    puts("--------------");
    printf("&str1: %#X\n   &n: %#X\n", &str1, &n);
    puts("--------------");
    printf("  &a: %#X\n arr: %#X\n  &b: %#X\n", &a, arr, &b);
    puts("--------------");
    printf("n: %d\na :%d\nb: %d\n", n, a, b);
    puts("--------------");
    printf("%s\n", pstr);
    system("pause");
    return 0;
}

#include <stdio.h>
#include <stdlib.h>
int getData();
int *getData2();
int *getData3();
int *getData4();
char *changeBubble2(char *arr1, char *arr2)int main(int argc, char const *argv[])
{
    int x = getData();
    printf("%d\n", x);
    int *x2 = getData2();
    printf("%d\n", *x2);
    int *x3 = getData3();
    printf("%d\n", *x3);

    int *x4 = getData4();
    printf("%d\n", *x4);
    free(x4);
    system("pause");
    return 0;
}

//可以返回数值,因为那是拷贝
int getData()
{
    int x = 2; //x在栈区,2在常量区
    return x;  //x将复制一份新的地址,地址指向2的内存空间,x自身的地址是会出栈的
}

//虽然可以返回得到x的值,但是不是安全的
//x可能随时被出栈,导致结果出问题
int *getData2()
{
    int x = 3;
    int *p = NULL;
    p = &x;
    return p; //地址没有变化,但是p的地址可能会出栈,导致数据出问题
}

//虽然可以返回得到x的值,但是不是安全的
int *getData3()
{
    int x = 4;
    return &x;
}

int *getData4()
{
    int *p = (int *)malloc(sizeof(int)); //分配在堆区,不会被销毁
    int x = 5;
    p = &x;//错误写法,不能在更改p的指针,否则内存无法回收,导致内存泄漏,另外,x的地址是会被销毁的
    *p = x; //将x的数值,复制给p
    return p;
}


//所有指向堆区的指针,地址是不能自行回收的
char *changeBubble2(char *arr1, char *arr2)
{
    char *result = (char *)malloc(strlen(arr1) + strlen(arr2) + 1); //1作为结束位置'\0',默认会添加'\0'
    if (result == NULL)
    {
        exit(1);
    }
    char *temp = result; //记录首地址,temp为堆区内存空间
    while (*arr1 != '\0')
    {
        *result = *arr1;
        arr1++;
        result++;
    }
    while (*arr2 != '\0')
    {
        *result = *arr2;
        arr2++;
        result++;
    }
    printf("%c\n", *result);
    return temp;
}

总结:

-------------引用赵四老师:
    a) : 栈中的变量通常包括函数参数和函数里声明的临时变量。
    b) : 栈中的基本变量退出其作用域时,没有谁执行一段代码去释放/销毁/析构它所占用的内存,仅仅是没人再去理会的留在当前栈顶上方的若干遗留下来可被后续压栈操作覆盖的无用数据而已。
    c) : 而栈中的类变量退出其作用域时,会自动执行其析构函数

所以你访问一个被释放的局部变量,都是一个不安全的行为。返回可以返回数值,但是不能返回一个局部变量的地址

原文链接:https://blog.csdn.net/Neutionwei/article/details/109629205

3.与java的内存模型完全不一致

堆:对象类型,包括Integer和Double等包装类型,都在堆区,使用都是地址复制

栈:基本类型,不区分局部变量,使用,都是拷贝

静态存储方法区(元数据):常量和static,常量池

本地方法栈:Native区, 主要是c/c++代码

——————主要就是这些。

Java 虚拟机栈:

它的生命周期与线程相同。个方法被执

行的时候都会同时创建一个栈帧(Stack Frame ①)用于存储局部变量表、操作栈、动态

链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在

虚拟机栈中从入栈到出栈的过程。

通过以上了解:我们可以知道为什么java会比C/C++慢了,原因是,除了基本类型对象,都是存储在堆区,自动去释放,参数的传递都是值传递(对象复制地址,基本类型全拷贝)

C/C++:有严格的堆栈区分,malloc手动创建数据在堆区

7.用户模式和内核模式

1.内核模式

在内核模式下,代码具有对硬件的所有控制权限。可以执行所有CPU指令,可以访问任意地址的内存。内核模式是为操作系统最底层,最可信的函数服务的。在内核模式下的任何异常都是灾难性的,将会导致整台机器停机。

——————一般都是系统,比如window操作系统

2.用户模式
在用户模式下,代码没有对硬件的直接控制权限,也不能直接访问地址的内存。程序是通过调用系统接口(System APIs)来达到访问硬件和内存。在这种保护模式下,即时程序发生崩溃也是可以恢复的。在你的电脑上大部分程序都是在用户模式下运行的。

8.一个函数在栈上到底是怎样的?

在main函数调用func_A的时候,首先在自己的栈帧中压入函数返回地址,然后为func_A创建新栈帧并压入系统栈
在func_A调用func_B的时候,同样先在自己的栈帧中压入函数返回地址,然后为func_B创建新栈帧并压入系统栈
在func_B返回时,func_B的栈帧被弹出系统栈,func_A栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到func_A代码区中执行
在func_A返回时,func_A的栈帧被弹出系统栈,main函数栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址跳到main函数代码区中执行

在实际运行中,main函数并不是第一个被调用的函数,程序被装入内存前还有一些其他操作,上图只是栈在函数调用过程中所起作用的示意图

————————进栈和出栈

9.函数出栈

(7)函数 func() 执行完成后开始出栈,首先将 edi、esi、ebx 寄存器的值出栈。

(8)将局部变量、返回值等数据出栈时,直接将 ebp 的值赋给 esp,这样 ebp 和 esp 就指向了同一个位置。

(9)接下来将 old ebp 出栈,并赋值给现在的 ebp,此时 ebp 就指向了 func() 调用之前的位置,即 main() 活动记录的 old ebp 位置,如步骤⑨所示。

5.4、遗留的错误认知

经过上面的分析可以发现,函数出栈只是在增加 esp 寄存器的值,使它指向上一个数据,并没有销毁之前的数据。前面我们讲局部变量在函数运行结束后立即被销毁其实是错误的,这只是为了让大家更容易理解,对局部变量的作用范围有一个清晰的认识。栈上的数据只有在后续函数继续入栈时才能被覆盖掉,这就意味着,只要时机合适,在函数外部依然能够取得局部变量的值。请看下面的代码:


#include <stdio.h>
 
int *p;
 
void func(int m, int n)
{
    int a = 18, b = 100;
    p = &a;
}
 
int main()
{
    int n;
    func(10, 20);
    n = *p;
    printf("n = %d\n", n);
    return 0;
}

注意:p获取到的是垃圾值,a的地址可能不存在了

原文链接:https://blog.csdn.net/Neutionwei/article/details/109695674

10.栈溢出攻击的原理是什么?

11.malloc函数背后的实现原理——内存池

12.野指针

野指针通常是因为指针变量中保存的值不是一个合法的内存地址而造成的。

合法的内存地址:

1.在堆空间动态申请的;

2.局部变量所在的栈。

野指针不是NULL指针,是指向不可用内存的指针,也可能是一个动态的内存地址,但是这个内存别人正在使用,这也是不合法的地址。

NULL指针不容易用错,因为if语句很好判断一个指针是不是NULL。C语言中没有任何手段可以判断一个指针是否为野指针!

#include 
char* func()
{
char p[] = "Delphi Tang";
return p;
}

int main()
{
char* s = func();
printf("%s\n", s); // OOPS!
return 0;
}

13.内存泄漏

保存的值不是一个合法的内存地址而造成的。

合法的内存地址:

1.在堆空间动态申请的;

2.局部变量所在的栈。

野指针不是NULL指针,是指向不可用内存的指针,也可能是一个动态的内存地址,但是这个内存别人正在使用,这也是不合法的地址。

NULL指针不容易用错,因为if语句很好判断一个指针是不是NULL。C语言中没有任何手段可以判断一个指针是否为野指针!

#include 
char* func()
{
char p[] = "Delphi Tang";
return p;
}

int main()
{
char* s = func();
printf("%s\n", s); // OOPS!
return 0;
}

13.内存泄漏

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值