malloc, free
Contents
strcpy
, strncpy
char *strcpy(char *dest, const char *src);
strcpy
在拷贝字符串时会把结尾的'\0'
也拷到dest
中,以保证dest
字符串以'\0'
结尾。strcpy
只知道src
字符串的首地址,不知道长度,它会一直拷贝到'\0'
为止,所以dest
所指向的内存空间要足够大,否则有可能写越界;如果没有保证src
所指向的内存空间以'\0'
结尾,也有可能读越界。strcpy
函数的实现者通过函数接口无法得知src
字符串的长度和dest
内存空间的大小,所以“确保不会写越界”应该是调用者的责任,调用者提供的dest
参数应该指向足够大的内存空间,“确保不会读越界”也是调用者的责任,调用者提供的src
参数指向的内存应该确保以'\0'
结尾。
src
和dest
所指向的内存空间不能有重叠。
char buf[10] = "hello";
// 不允许
strcpy(buf, buf+1);
dest
指针本身就是调用者传过去,再返回一遍dest
指针并没有提供任何有用信息。- 这么规定是为了把函数调用当作一个指针类型的表达式使用,比如
printf("%s\n", strcpy(buf, "hello"))
;如果strcpy
的返回值是void
就没有这么方便。 - 凡是有指针参数的 C 标准库函数基本都要求每个指针参数所指向的内存空间互不重叠。
char *strncpy(char *dest, const char *src, size_t n);
strcpy
比strncpy
更加不安全,调用前不仔细检查src
字符串的长度就有可能写越界。
void foo(char *str)
{
char buf[10];
strcpy(buf, str);
// ...
}
str
所指向的字符串有可能超过 10 个字符而导致写越界。- 这种写越界可能当时不出错,而在函数返回时出现段错误,原因是写越界覆盖了保存在栈帧上的返回地址(返回到该地址继续执行代码),函数返回时跳转到非法地址,因而出错。
像 buf
这种由调用者分配并传给函数读或写的一段内存通常称为缓冲区(Buffer),缓冲区写越界的错误称为缓冲区溢出(Buffer Overflow)。
- 只是出现段错误还不算严重,更严重的是缓冲区溢出 Bug 经常被恶意用户利用,使函数返回时跳转到一个事先设好的地址,执行事先设好的指令。
- 如果设计得巧妙甚至可以启动一个 Shell,然后随心所欲执行任何命令;如果一个用 root 权限执行的程序存在这样的 Bug,被攻陷了,后果很严重。
-
malloc
, free
动态分配一块内存可以定义一个缓冲区数组,但这种方法不够灵活:C89 要求定义的数组是固定长度的,而程序往往在运行时才知道要动态分配多大的内存。
C99 引入 VLA 特性,可以定义 char buf[n+1] = {};
,这样可确保 buf
以 '\0'
结尾。
- VLA 仍然不够灵活,VLA 在栈上动态分配,函数返回时就要释放,如果希望动态分配一块全局的内存空间,而全局数组无法定义成 VLA,所以仍然不能满足要求。
进程有一个堆空间,C 标准库函数 malloc
可以在堆空间动态分配内存。
malloc
底层通过brk
系统调用向操作系统申请内存。- 动态分配的内存用完之后可以用
free
释放,归还给malloc
,下次调用malloc
时这块内存可以再次被分配。free
的参数是先前malloc
返回的内存块首地址。
#include <stdlib.h>
void *malloc(size_t size);
// 返回值:成功返回所分配内存空间的首地址,出错返回 NULL
void free(void *ptr);
malloc
的参数size
表示要分配的字节数,如果分配失败(比如系统内存耗尽)则返回 NULL。malloc
不知道用户要存放的数据类型,所以返回通用指针void *
,用户程序可以转换成其它类型的指针再访问这块内存。malloc
函数保证它返回的指针所指向的地址满足系统的对齐要求。- 例如在 32 位平台上返回的指针一定对齐到 4 字节边界,以保证用户程序把它转换成任何类型的指针都能用。
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int number;
char *msg;
} unit_t;
int main(void)
{
unit_t *p = malloc(sizeof(unit_t));
if (p == NULL) {
printf("out of memory\n");
exit(1);
}
p->number = 3;
p->msg = malloc(20);
strcpy(p->msg, "Hello world!");
printf("number: %d\nmsg: %s\n", p->number, p->msg);
free(p->msg);
free(p);
p = NULL;
return 0;
}
unit_t *p = malloc(sizeof(unit_t));
等号右边是void *
类型,等号左边是unit_t *
类型,编译器会做隐式类型转换。void *
类型和任何指针类型之间都可以相互隐式转换。
malloc
之后应该判断是否申请成功。- 大部分系统函数都有成功的返回值和失败的返回值,每次调用系统函数都应该判断是否成功。
- 要先
free(p->msg)
再free(p)
。如果先free(p)
,p
就成了野指针,不能再通过p->msg
访问到。 free(p);
之后,归还了p
所指向的内存空间,但p
的值并没有变,free
函数不能改变p
的值,p
现在指向的内存空间已经不属于用户,也就是说p
成了野指针;所以为避免出现野指针,应该在free(p);
之后手动置p = NULL;
。
简单的程序即使不用 free
释放内存也可以,因为程序退出时整个进程地址空间都会释放,包括堆空间,该进程占用的所有内存都会归还给操作系统。但如果一个程序长期运行(如网络服务器程序),并且在循环或递归中调用 malloc
分配内存,则必须有 free
与之配对,分配一次就要释放一次,否则就会慢慢耗尽系统内存,称为内存泄漏(Memory Leak)。
- 大量的内存泄漏会使系统内存紧缺,导致频繁换页,不仅影响当前进程,而且会把整个系统都拖得很慢。
malloc
返回的指针一定要保存好,只有把它传给free
才能释放这块内存,如果这个指针丢失了,就没有办法free
这块内存,造成内存泄漏。
malloc
和 free
基于环形链表的简单实现:
)
- 白色背景的框表示
malloc
管理的空闲内存块。 - 深色背景的框不归
malloc
管,可能是已经分配给用户的内存块,也可能不属于当前进程。 - Break 之上的地址不属于当前进程,需要通过
brk
系统调用向内核申请。 - 每个内存块开头都有一个头节点,里面有一个指针字段,指向下一个头结点和一个长度字段(自己管理的内存的大小)。
- 指针字段把所有不连续的空闲块的头节点串在一起,组成一个环形链表。
- 长度字段记录着头节点和后面的内存块加起来一共有多长。
- 内存块大小是$$长度字段 * 8 字节$$;8 字节是选取头节点的长度作为单位。
- 操作分析:
- 一开始堆空间由一个空闲块组成,长度为 $$7×8=56$$ 字节;
- 头节点本身占 8 字节。
- 调用
malloc
分配 8 字节后,在空闲块的末尾截出 16 个字节的内存块;- 新的头节点占 8 个字节;另外 8 个字节返回给用户使用,返回的指针
p1
指向头节点后面的内存块。
- 新的头节点占 8 个字节;另外 8 个字节返回给用户使用,返回的指针
- 再次调用
malloc
分配 16 字节后,又在空闲块的末尾截出 24 个字节的内存块,步骤和上一步类似; - 调用
free
释放p1
所指向的内存块,内存块(包括头节点在内)归还给了malloc
,现在malloc
管理着两块不连续的内存,用环形链表串起来;- 此时
p1
成了野指针,指向不属于用户的内存,p1
所指向的内存地址在 Break 之下,属于当前进程,所以访问p1
时不会出现段错误,但在访问p1
时这段内存可能已经被malloc
再次分配出去了,可能会读到意外改写数据。 - 此时如果通过
p2
向右写越界,有可能覆盖右边的头节点,从而破坏malloc
管理的环形链表,malloc
就无法从一个空闲块的指针字段找到下一个空闲块了。
- 此时
- 调用
malloc
分配 16 个字节,虽然有两个空闲块,各有 8 个字节可分配,但是这两块不连续,malloc
只好通过brk
系统调用抬高 Break,获得新的内存空间; - 新申请的空闲块和前一个空闲块连续,因此可以合并成一个;在能合并时要尽量合并,以免空闲块越割越小,无法满足大的分配请求;
- 在合并后的这个空闲块末尾截出 24 个字节,新的头节点占 8 个字节,另外 16 个字节返回给用户;
- 调用
free(p3)
释放这个内存块,由于它和前一个空闲块连续,又重新合并成一个空闲块;- Break 只能抬高而不能降低,从内核申请到的内存以后都归
malloc
管,即使调用free
也不会还给内核。
- Break 只能抬高而不能降低,从内核申请到的内存以后都归
- 一开始堆空间由一个空闲块组成,长度为 $$7×8=56$$ 字节;
References