ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 内存空间的有名与无名 现在来看两行 C 代码: ~~~ int foo = 10; int *bar = &foo; ~~~ `foo` 是什么?`foo` 表示一个内存地址。`foo` 前面的 `int` 是数据类型修饰,它表示 `foo` 是内存中 4 个连续字节的首字节地址( 64 位机器上,`int` 类型的数据长度为 4 个字节)。C 编译器总是会根据某个内存地址相应的类型来确定以该内存地址起始的一段连续字节中所存储的数据的逻辑意义。因此,当我们用 `int` 类型来修饰 `foo`,编译器就会认为以 `foo` 开始的连续 4 个字节中存储的数据是一个整型数据。在上述代码中,这个整型数据是 `10`,我们通过赋值运算符 `=` 将这个整型数保存到内存中以 `foo` 地址开始的连续 4 个字节中。 从此刻开始,要记住一个事实,那就是 C 语言中所有的变量名,本质上都是内存地址。之所以不直接使用内存地址,而是使用一些有意义的名字,这就类似于没人愿意用你的身份证号来称呼你,大家更愿意用你的姓名来称呼你。 由于 C 语言认为数据的长度是由其类型确定的。例如,`int` 类型的数据长度是 4 个字节,`char` 类型的数据长度是是 1 个字节,用户自定义的 `struct` 类型的数据长度则是根据实际情况而待定。在这种情况下,所有表示内存地址的名字,它们实质上表示的是内存中各种类型数据存储空间的起始地址——专业一点,就是基地址。凡是用名字来表示基地址的内存空间,我们就将其称为有名的内存空间。 再来看 `bar` 是什么?`bar` 是内存地址的名字,由于 `bar` 前面有个 `*` 号,这表示我们打算在以 bar 为基地址的连续 8 个字节中存储一个内存地址(别忘了,我们是在 64 位机器上,指针数据的长度是 8 个字节)——`foo` 所表示的那个地址,亦即 `&foo`。在这里, `&` 是取值符,它会对 `foo` 说,你甭给我耍花样了,老实交代你的身份证号!在`*` 之前还有 `int`,这意味着在以 bar 为基地址的连续 8 个字节中存储的那个内存地址是某个用于存储整型数据的内存空间的基地址。 由于 `bar` 是某个内存空间的基地址,而这个内存空间中存储的是一个内存地址,所以 `bar` 就是所谓的指针。在这里,我们可以认为 `bar` 是对某块以 `foo` 为基地址的内存空间的『引用』,也就是在一个房间号为 `bar` 的房间里存储了房间号`foo`。按照 C 语言教材里常用的说法,可将 `int *bar = &foo` 这件事描述为『指针 `bar` 指向了整型变量 `foo`』,然而事实上内存里哪有什么针,哪有什么指向?一切都是内存空间的引用。在上面的例子里,我们是用 `foo` 来直接引用某个内存空间,然后又使用 `bar` 来间接引用某个内存空间。 在上面的例子里,`bar` 引用的是一个有名的内存空间。那么有没有无名的内存空间呢?看下面的代码: ~~~ int *bar = malloc(sizeof(int)); ~~~ `malloc(sizeof(int))` 就是一个无名的内存空间,因为它是一个表达式,而这个表达式描述的是一系列行为,行为需要借助动词来描述,而无法用名词来描述。比如『我在写文章』,这种行为无法只使用名词来描述,必须借助动词。任何会终止的行为都可表示为一系列的状态的变化,也就是说任何会终止的行为都会产生一个结果,而这个结果可以用名词来描述。例如 `malloc(sizeof(int))` 这个行为就是可终止的,它的结果是它在内存所开辟 4 个字节的空间的基地址,这个基地址是没有名字的,所以它就是个无名的基地址,因此它对应的内存空间就是无名的内存空间,但是如果我们想访问这个空间,就必须为它取个名字,当我们用 `bar` 指针引用它的基地址时,它就变成有名的了。 C 语言的创始人—— Dennis Ritchie 与 Brian Kernighan 将带名字的存储空间称为对象(Object)——并非『面向对象编程』中的对象,然后将指代这个对象的表达式称为左值(lvalue)。也就是说,在 C 语言中,上例中的 `foo` 与 `bar` 都是左值,因为它们总是能够出现在赋值符号的左侧。 看下面的代码: ~~~ int foo = 10; int *bar = &foo; printf("%d", *bar); ~~~ 第三行的 `printf` 语句中的 `*bar` 也是一个左值,因为它指代了一个有名字的存储空间,这个存储空间的名字就叫做`*bar`。这个存储空间其实就是以 `foo` 为基地址的存储空间。在表达式 `*bar` 中, `*` 号的作用是解引用,就是将以 `bar`为基地址的内存空间中存储的内存地址取出来,然后去访问这个内存地址对应的内存空间。由于 `*bar` 的类型是 `int`,所以程序自身就可以知道要访问的是以 `*bar` 为基地址的 4 个字节,因此它可以准确无误的将整型数据 `10` 取出来并交给`printf` 来显示。 指针最黑暗之处在于,当你拿到了一块内存空间的基地址之后,你可以借助这个基地址随意访问内存中的任何区域!也就是说,你可以从通过指针获得内存空间的入口,然后你可以让你的程序在内存中(栈空间)随便逛,随便破坏,然后你的程序可能就崩溃了。你的程序如果隐含缓冲区溢出漏洞,它甚至可被其他程序控制着去执行一些对你的系统非常不利的代码,这就是所谓的缓冲区溢出攻击。C 语言不提供任何缓冲区保护机制,能否有效保护缓冲区,主要取决于你的 C 编程技艺。 > 现在我们写 C 程序时,基本上不需要担心自己的程序会遭遇缓冲区溢出攻击。因为只有那些被广泛使用的 C 程序才有这种风险;如果很不幸,你写的 C 程序真的被很多人使用了,那也不需要太担心。《深入理解计算机系统》在 3.12 节『存储器的越界引用和缓冲区溢出』中告诉我们,现代操作系统对程序运行时所需要的栈空间是随机生成的,导致攻击者很难获得栈空间中的某个确定地址,至少在 Linux 系统中是这样子。C 语言编译器提供了栈破坏检测——至少在 GCC 中是这样,其原理就是程序的栈空间放置了一只『金丝雀』,程序在运行中一旦发现有袭击『金丝雀』的可耻代码,它就会异常终止。处理器层面也对可执行代码所在的内存区域进行了限定,这样攻击者很难再向程序的栈空间插入攻击系统的可执行代码了。