全文以SSD6 Exercise3为例。
按照题意,加上额外的信息,在malloc里类似这样做。
void *MyMalloc(size_t size, char *filename, int linenumber) {
void* p = malloc(strlen(filename) + sizeof(linenumber) + sizeof(fence) + size + sizeof(fence));
void* res = (void*)((int*)((char*)p + sizeof(filename)) + sizeof(linenumber) + sizeof(fence));
return res;
}
但是这样做的话,在free里很难知道有效载荷(PayLoad)是多长,于是查找资料。
malloc和free函数详解 - Healtheon - 博客园 (cnblogs.com)
这篇文章给了我灵感,可以使用结构体来封装有效载荷和需要添加的额外的数据。
这里没法把filename放到header里,因为我们要存两个指针,一个是header一个是block,block是要通过hader+sizeof header计算的,所以header的大小必须是固定的。
typedef struct Header {
int checkSum;
int fence;
}HEADER;
typedef struct Footer {
int fence;
}Footer;
typedef struct Block {
HEADER* head;
void* payLoad;
Footer* foot;
char* filename;
int linenumber;
}BLOCK;
下面开始写MyMalloc函数。
思路是首先创建header,size加上footer这三部分,filename先放一放,因为我们不知道它的固定长度。
先不考虑边界对齐,代码长这个样子。
void *MyMalloc(size_t size, char *filename, int linenumber) {
void* block_begin = NULL;
if (!(block_begin = malloc(sizeof(Header) + size + sizeof(Footer)))) {
return NULL;
}
//int cnt = malloc_usable_size(block_begin);
//printf("%d", cnt);
void* ptr = (Header*)block_begin + 1;
Footer* footer = (char*)ptr + AlignBorder(size);
Header head;
head.fence = FENCE;
head.size = size;
head.checkSum = 1;
memcpy(block_begin, &head, sizeof(Header));
Footer foot;
foot.fence = FENCE;
memcpy(footer, &foot, sizeof(Footer));
// Create Block
Blocker block;
return ptr;
}
在做MyFree的时候,发现需要拿到MyMalloc的文件名和行号,看来还是需要存一个Block列表的。
Block列表大体需要三个方法,addToList,removeFromList和findInList。
首先,为了语义通顺,这三个函数的参数使用的都是PayLoad的指针而不是head或block的指针。
addToList主要做四件事:
- 分配block的内存。
- 分配filename的内存。
- 初始化block。
- 将block插入列表中。
nt addToList(void* ptr, char* filename, int linenumber) {
Header* header = (Header*)ptr - 1;
Footer* footer = (Footer*)((char*)ptr + header->size);
// Allocate memory for block
Block* block = malloc(BLOCKSIZE);
if (!block) {
return -1;
}
// Allocate memory for filename
size_t len = strlen(filename) + 1;
char* name = (char*)malloc(len);
if (!name) {
return -1;
}
strcpy_s(name, strlen(filename) + 1, filename);
// Initailize block
block->head = header;
block->payLoad = ptr;
block->foot = footer;
block->filename = name;
block->linenumber = linenumber;
// Head plug
block->next = block_head;
block_head = block;
return 0;
}
removeFromList做的三件事:
- 遍历链表并将目标结点删除。
- 释放目标节点申请的filename的内存。
- 释放目标节点申请的block的内存。
// Remove Block
int removeFromList(void* ptr) {
Block* head = block_head;
Block* pre = NULL;
// traverse
while (head) {
if (head->payLoad == ptr) {
if (pre) {
pre->next = head->next;
}
else { // situation of no pre node
block_head = block_head->next;
}
// free
free(head->filename);
head->filename = NULL;
free(head);
head = NULL;
return 0;
}
pre = head;
head = head->next;
}
return -1;
}
最后是findInList,这个就很简单了,遍历链表找到目标节点并返回即可。
// Find Block
Block* findInList(void* ptr) {
Block* b = block_head;
while (b){
if (b->payLoad == ptr){
return b;
}
b = b->next;
}
return NULL;
}
将上面三个函数写好之后,继续在MyMalloc里面做修改,将ptr指向的空间申请好之后将必要的几个信息整合为Block添加到链表中,整合的代码在addToList里做了,这里调用一下即可。
// Create Block
if (addToList((void*)ptr, filename, linenumber) == 0)
{
//allocatedSize += h.size;
return (void*)(ptr);
}
else
{
free(block_begin);
return NULL;
}
最后是MyFree函数,这里我们释放之前需要先判断一下错误,于是封装了一个检查错误的函数。
根据题意:
- 当向前越界(头部的fence被破坏)时,返回1.
- 当向后越界(尾部的fence被破坏)时,返回2
- 当checkSum被修改时,返回3
- 没有任何问题,返回0
int check_block(void* ptr) {
Header* header = (Header*)ptr - 1;
Footer* footer = (Footer*)((char*)ptr + header->size);
if (header->fence != FENCE) {
return 1;
}else if (footer->fence != FENCE) {
return 2;
}else if (header->checkSum != 1){
return 3;
}
return 0;
}
第四项错误是,double free,这个错误可以在MyFree里直接判断。
于是MyFree长这个样子。
void MyFree(void* ptr, char* filename, int linenumber) {
Header* head = (Header*)ptr - 1;
Block* block = findInList(ptr);
if (!block) {
error(4, filename, linenumber);
}
int num = check_block(ptr);
if (num) errorfl(num, block->filename, block->linenumber, filename, linenumber);
allocatedSize -= head->size;
free(head);
head = NULL;
removeFromList(ptr);
}
大体上就是这些内容了,后面还有两个简单的小函数,打印列表和检测剩余堆内存的函数。
/* Prints a list of all allocated blocks with the
filename/line number when they were MALLOC'd */
void PrintAllocatedBlocks() {
Block* head = block_head;
while (head) {
printf("filename: %s, linenumber: %d", head->filename, head->linenumber);
head = head->next;
}
return;
}
/* Goes through the currently allocated blocks and checks
to see if they are all valid.
Returns -1 if it receives an error, 0 if all blocks are
okay.
*/
int HeapCheck() {
Block* head = block_head;
while (head) {
int num = check_block(head->payLoad);
if (num) {
PRINTERROR(num, head->filename, head->linenumber);
return -1;
}
head = head->next;
}
return 0;
}
总结
内存错误(memory bugs)大体上分为五种:
- 前向越界
- 后向越界
- 校验和错误
- double free
- Memory leak
前三个都很好检测,重点是后面两个,doule free是通过遍历链表查看堆链表中没有目标指针,说明已经被释放过或者压根儿就没创建过,这里要进行报错。接着,内存泄漏是通过allocatedSize变量记录的值来判断的,如果大于零说明有未被释放的内存,然后接着遍历列表打印,从而可以知道未被释放的内存是哪里创建的。
处理上述问题所用到的结构体主要是Block,它的主要成员变量指针malloc时的关系图如下图所示。
ps:case3有边界对齐的问题,32位系统应该是4个字节取一次地址,但是没有写这部分代码并不影响结果,查看内存发现malloc申请时并未遵循边界对齐的原则,可能是不同编译器的不同策略。
case 3: { /* should overflow by 1, harder to catch
because of alignment */
char* str = (char*)MALLOC(2);
strcpy_s(str, strlen(str), "12");
FREE(str);
}
int AlignBorder(size_t size) {
if ((size & 0x3) == 0) {
return size;
}
else {
return (size >> 2 + 1) << 2;
}
}
(6条消息) 位运算取余_actionzh的博客-CSDN博客_位运算取余
使用位运算取余提高效率,如果不是4的倍数,则右移加一再左移得到的是比size大的最小的4的倍数的数。
如下图,两字节申请是紧紧挨着Header和Footer的。