阅记-计算机系统-Hack the Virtual Memory 3: malloc, the heap & the program break
link: Hack the Virtual Memory: malloc, the heap & the program break
- The heap
- malloc
- brk and sbrk
- many mallocs - 首次malloc brk() create new space(by increasing the program break location), 后续malloc仍使用该空间 give program new chunk
- naive malloc
- ===
- The 0x10 lost bytes
- The first 8 bytes of the 16 (0x10 in hexadecimal) bytes
- Is the heap actually growing upwards?
- Address Space Layout Randomisation (ASLR)
- The updated VM diagram
- malloc(0)
- Outro
The heap
In this chapter we will look at the heap and malloc in order to answer some of the questions we ended with at the end of the previous chapter:
- Why doesn’t our allocated memory start at the very beginning of the heap (0x2050010 vs 02050000)? What are those first 16 bytes used for? 为什么我们分配的内存不是从堆的最初开始 (0x2050010 对 02050000)? 前 16 字节用来做什么?
- Is the heap actually growing upwards? 堆真的在向上增长吗?
malloc
malloc is the common function used to dynamically allocate memory. This memory is allocated on the “heap”.
Note: malloc is not a system call.
From man malloc
:
[...] allocate dynamic memory[...]
void *malloc(size_t size);
[...]
The malloc() function allocates size bytes and returns a pointer to the allocated memory.
no malloc, no [heap]
Let’s look at memory regions of a process that does not call malloc (0-main.c
).
点击查看代码
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
getchar();
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 0-main.c -o 0
$ ./0
$ ps aux | grep \ \./0$
julien 3638 0.0 0.0 4200 648 pts/9 S+ 12:01 0:00 ./0
$ cat /proc/3638/maps
00400000-00401000 r-xp 00000000 08:01 174583 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/0
00600000-00601000 r--p 00000000 08:01 174583 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/0
00601000-00602000 rw-p 00001000 08:01 174583 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/0
7f38f87d7000-7f38f8991000 r-xp 00000000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f38f8991000-7f38f8b91000 ---p 001ba000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f38f8b91000-7f38f8b95000 r--p 001ba000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f38f8b95000-7f38f8b97000 rw-p 001be000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f38f8b97000-7f38f8b9c000 rw-p 00000000 00:00 0
7f38f8b9c000-7f38f8bbf000 r-xp 00000000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f38f8da3000-7f38f8da6000 rw-p 00000000 00:00 0
7f38f8dbb000-7f38f8dbe000 rw-p 00000000 00:00 0
7f38f8dbe000-7f38f8dbf000 r--p 00022000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f38f8dbf000-7f38f8dc0000 rw-p 00023000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f38f8dc0000-7f38f8dc1000 rw-p 00000000 00:00 0
7ffdd85c5000-7ffdd85e6000 rw-p 00000000 00:00 0 [stack]
7ffdd85f2000-7ffdd85f4000 r--p 00000000 00:00 0 [vvar]
7ffdd85f4000-7ffdd85f6000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
-> As we can see from the above maps file, there’s no [heap] region allocated.
malloc(x)
Let’s do the same but with a program that calls malloc (1-main.c
):
点击查看代码
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
malloc(1);
getchar();
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 1-main.c -o 1
$ ./1
$ ps aux | grep \ \./1$
julien 3718 0.0 0.0 4332 660 pts/9 S+ 12:09 0:00 ./1
$ cat /proc/3718/maps
00400000-00401000 r-xp 00000000 08:01 176964 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/1
00600000-00601000 r--p 00000000 08:01 176964 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/1
00601000-00602000 rw-p 00001000 08:01 176964 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/1
01195000-011b6000 rw-p 00000000 00:00 0 [heap]
...
-> the [heap] is here.
+++
Let’s check the return value of malloc to make sure the returned address is in the heap region (2-main.c
):
#include <stdio.h>
#include <stdlib.h>
int main(void) {
void *p;
p = malloc(1);
printf("%p\n", p);
getchar();
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 2-main.c -o 2
$ ./2
0x24d6010
$ ps aux | grep \ \./2$
julien 3834 0.0 0.0 4336 676 pts/9 S+ 12:48 0:00 ./2
$ cat /proc/3834/maps
00400000-00401000 r-xp 00000000 08:01 176966 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/2
00600000-00601000 r--p 00000000 08:01 176966 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/2
00601000-00602000 rw-p 00001000 08:01 176966 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/2
024d6000-024f7000 rw-p 00000000 00:00 0 [heap]
...
-> 024d6000 < 0x24d6010 < 024f7000
The returned address is inside the heap region. And as we have seen in the previous chapter.
the returned address does not start exactly at the beginning of the region; we’ll see why later.
多的0x10 bytes用于chunk malloc/free管理
brk and sbrk
malloc is a "regular" function (as opposed to a system call), so it must call some kind of syscall in order to manipulate the heap. Let’s use strace
to find out. strace
is a program used to trace system calls and signals. Any program will always use a few syscalls before your main
function is executed. In order to know which syscalls are used by malloc, we will add a write
syscall before and after the call to malloc(3-main.c
).
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
void *p;
write(1, "BEFORE MALLOC\n", 14);
p = malloc(1);
write(1, "AFTER MALLOC\n", 13);
printf("%p\n", p);
getchar();
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 3-main.c -o 3
$ strace ./3
execve("./3", ["./3"], [/* 61 vars */]) = 0
...
write(1, "BEFORE MALLOC\n", 14BEFORE MALLOC
) = 14
brk(0) = 0xe70000
brk(0xe91000) = 0xe91000
write(1, "AFTER MALLOC\n", 13AFTER MALLOC
) = 13
...
read(0,
From the above listing we can focus on this:
brk(0) = 0xe70000
brk(0xe91000) = 0xe91000
-> malloc is using the brk
system call in order to manipulate the heap. From brk
man page (man brk), we can see what this system call is doing:
...
int brk(void *addr);
void *sbrk(intptr_t increment);
...
DESCRIPTION
brk() and sbrk() change the location of the program break, which defines
the end of the process's data segment (**i.e., the program break is the first location after the end of the uninitialized data segment**).
Increasing the program break has the effect of allocating memory to the process,
decreasing the break deallocates memory.
brk() sets the end of the data segment to the value specified by addr, when
that value is reasonable, the system has enough memory, and the process
does not exceed its maximum data size (see setrlimit(2)).
sbrk() increments the program's data space by increment bytes. Calling
sbrk() with an increment of 0 can be used to find the current location of
the program break.
简译:brk和sbrk都是change program break location的api, brk通过指定the end of the process's data segment来扩大空间,sbrk直接指定要多扩大多少空间
The program break is the address of the first location beyond the current end of the data region of the program in the virual memory.
program break 是程序data段后(i.e., the program break is the first location after the end of the uninitialized data segment)的第一个地址
By increasing the value of the program break, via brk
or sbrk
, the function malloc creates a new space that can then be used by the process to dynamically allocate memory (using malloc).
通过brk或sbrk增加program break值,函数malloc创建了一个新的空间,然后进程可以使用该空间动态分配内存(使用malloc)。
So the heap is actually an extension of the data segment of the program.
+++
The first call to brk
(brk(0)
) returns the current address of the program break to malloc. And the second call is the one that actually creates new memory (since 0xe91000
> 0xe70000
) by increasing the value of the program break. In the above example, the heap is now starting at 0xe70000
and ends at 0xe91000
, check with the /proc/[PID]/maps
file:
$ ps aux | grep \ \./3$
julien 4011 0.0 0.0 4748 708 pts/9 S+ 13:04 0:00 strace ./3
julien 4014 0.0 0.0 4336 644 pts/9 S+ 13:04 0:00 ./3
$ cat /proc/4014/maps
00400000-00401000 r-xp 00000000 08:01 176967 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/3
00600000-00601000 r--p 00000000 08:01 176967 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/3
00601000-00602000 rw-p 00001000 08:01 176967 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/3
00e70000-00e91000 rw-p 00000000 00:00 0 [heap]
...
-> 00e70000-00e91000 rw-p 00000000 00:00 0 [heap]
matches the pointers returned back to malloc by brk
.
+++
That’s great, but wait, why did malloc increment the heap by 00e91000 – 00e70000 = 0x21000
or 135168
bytes, when we only asked for only 1 byte?
程序只申请1个字节,为什么malloc会将堆增加“00e91000”-“00e70000”=“0x21000”或“135168”字节?
many mallocs - 首次malloc brk() create new space(by increasing the program break location), 后续malloc仍使用该空间 give program new chunk
What will happen if we call malloc several times? (4-main.c
)
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
void *p;
write(1, "BEFORE MALLOC #0\n", 17);
p = malloc(1024);
write(1, "AFTER MALLOC #0\n", 16);
printf("%p\n", p);
write(1, "BEFORE MALLOC #1\n", 17);
p = malloc(1024);
write(1, "AFTER MALLOC #1\n", 16);
printf("%p\n", p);
write(1, "BEFORE MALLOC #2\n", 17);
p = malloc(1024);
write(1, "AFTER MALLOC #2\n", 16);
printf("%p\n", p);
write(1, "BEFORE MALLOC #3\n", 17);
p = malloc(1024);
write(1, "AFTER MALLOC #3\n", 16);
printf("%p\n", p);
getchar();
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 4-main.c -o 4
$ strace ./4
execve("./4", ["./4"], [/* 61 vars */]) = 0
...
write(1, "BEFORE MALLOC #0\n", 17BEFORE MALLOC #0
) = 17
brk(0) = 0x1314000
brk(0x1335000) = 0x1335000
write(1, "AFTER MALLOC #0\n", 16AFTER MALLOC #0
) = 16
...
write(1, "0x1314010\n", 100x1314010
) = 10
write(1, "BEFORE MALLOC #1\n", 17BEFORE MALLOC #1
) = 17
write(1, "AFTER MALLOC #1\n", 16AFTER MALLOC #1
) = 16
write(1, "0x1314420\n", 100x1314420
) = 10
write(1, "BEFORE MALLOC #2\n", 17BEFORE MALLOC #2
) = 17
write(1, "AFTER MALLOC #2\n", 16AFTER MALLOC #2
) = 16
write(1, "0x1314830\n", 100x1314830
) = 10
write(1, "BEFORE MALLOC #3\n", 17BEFORE MALLOC #3
) = 17
write(1, "AFTER MALLOC #3\n", 16AFTER MALLOC #3
) = 16
write(1, "0x1314c40\n", 100x1314c40
) = 10
...
read(0,
malloc is NOT calling brk
each time we call it.
The first time, malloc creates a new space (the heap) for the program (by increasing the program break location). The following times, malloc uses the same space to give our program “new” chunks of memory. Those “new” chunks of memory are part of the memory previously allocated using brk
. This way, malloc doesn’t have to use syscalls (brk
) every time we call it, and thus it makes malloc – and our programs using malloc – faster. It also allows malloc and free to optimize the usage of the memory.
Let’s check that have only one heap, allocated by the first call to brk
: Let's 再次检查一下,确保只有一个堆,这个堆是通过第一次调用 brk 分配的:
$ ps aux | grep \ \./4$
julien 4169 0.0 0.0 4748 688 pts/9 S+ 13:33 0:00 strace ./4
julien 4172 0.0 0.0 4336 656 pts/9 S+ 13:33 0:00 ./4
$ cat /proc/4172/maps
00400000-00401000 r-xp 00000000 08:01 176973 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/4
00600000-00601000 r--p 00000000 08:01 176973 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/4
00601000-00602000 rw-p 00001000 08:01 176973 /home/julien/holberton/w/hack_the_virtual_memory/03. The Heap/4
01314000-01335000 rw-p 00000000 00:00 0 [heap]
7f4a3f2c4000-7f4a3f47e000 r-xp 00000000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f4a3f47e000-7f4a3f67e000 ---p 001ba000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f4a3f67e000-7f4a3f682000 r--p 001ba000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f4a3f682000-7f4a3f684000 rw-p 001be000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f4a3f684000-7f4a3f689000 rw-p 00000000 00:00 0
7f4a3f689000-7f4a3f6ac000 r-xp 00000000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f4a3f890000-7f4a3f893000 rw-p 00000000 00:00 0
7f4a3f8a7000-7f4a3f8ab000 rw-p 00000000 00:00 0
7f4a3f8ab000-7f4a3f8ac000 r--p 00022000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f4a3f8ac000-7f4a3f8ad000 rw-p 00023000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f4a3f8ad000-7f4a3f8ae000 rw-p 00000000 00:00 0
7ffd1ba73000-7ffd1ba94000 rw-p 00000000 00:00 0 [stack]
7ffd1bbed000-7ffd1bbef000 r--p 00000000 00:00 0 [vvar]
7ffd1bbef000-7ffd1bbf1000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
julien@holberton:/proc/4172$
-> We have only one [heap] and the addresses match those returned by brk
: 0x1314000
& 0x1335000
naive malloc
Based on the above, and assuming we won’t ever need to free anything, we can now write our own (naive) version of malloc, that would move the program break each time it is called.
点击查看代码
#include <stdlib.h>
#include <unistd.h>
/**
* malloc - naive version of malloc: dynamically allocates memory on the heap using sbrk
* @size: number of bytes to allocate
*
* Return: the memory address newly allocated, or NULL on error
*
* Note: don't do this at home :)
*/
void *malloc(size_t size)
{
void *previous_break;
previous_break = sbrk(size);
/* check for error */
if (previous_break == (void *) -1) {
/* on error malloc returns NULL */
return (NULL);
}
return (previous_break);
}
===
The 0x10 lost bytes
If we look at the output of the previous program (4-main.c), we can see that the first memory address returned by malloc doesn’t start at the beginning of the heap, but 0x10
bytes after: 0x1314010 vs 0x1314000.
Also, when we call malloc(1024) a second time, the address should be 0x1314010 (the returned value of the first call to malloc) + 1024 (or 0x400 in hexadecimal, since the first call to malloc was asking for 1024 bytes) = 0x1318010. But the return value of the second call to malloc is 0x1314420
. We have lost 0x10
bytes again!
Let’s look at what we can find inside those “lost” 0x10
-byte memory spaces (5-main.c) and whether the memory loss stays constant:
5-main.c: 打印0x10 lost bytes - one clear pattern: the size of the malloc’ed memory chunk is always found in the preceding 0x10 bytes.
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void pmem(void *p, unsigned int bytes)
{
unsigned char *ptr;
unsigned int i;
ptr = (unsigned char *)p;
for (i = 0; i < bytes; i++)
{
if (i != 0)
{
printf(" ");
}
printf("%02x", *(ptr + i));
}
printf("\n");
}
int main(void)
{
void *p;
int i;
for (i = 0; i < 10; i++)
{
p = malloc(1024 * (i + 1));
printf("%p\n", p);
printf("bytes at %p:\n", (void *)((char *)p - 0x10));
pmem((char *)p - 0x10, 0x10);
}
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 5-main.c -o 5
$ ./5
0x1fa8010
bytes at 0x1fa8000:
00 00 00 00 00 00 00 00 11 04 00 00 00 00 00 00
0x1fa8420
bytes at 0x1fa8410:
00 00 00 00 00 00 00 00 11 08 00 00 00 00 00 00
0x1fa8c30
bytes at 0x1fa8c20:
00 00 00 00 00 00 00 00 11 0c 00 00 00 00 00 00
0x1fa9840
bytes at 0x1fa9830:
00 00 00 00 00 00 00 00 11 10 00 00 00 00 00 00
0x1faa850
bytes at 0x1faa840:
00 00 00 00 00 00 00 00 11 14 00 00 00 00 00 00
0x1fabc60
bytes at 0x1fabc50:
00 00 00 00 00 00 00 00 11 18 00 00 00 00 00 00
0x1fad470
bytes at 0x1fad460:
00 00 00 00 00 00 00 00 11 1c 00 00 00 00 00 00
0x1faf080
bytes at 0x1faf070:
00 00 00 00 00 00 00 00 11 20 00 00 00 00 00 00
0x1fb1090
bytes at 0x1fb1080:
00 00 00 00 00 00 00 00 11 24 00 00 00 00 00 00
0x1fb34a0
bytes at 0x1fb3490:
00 00 00 00 00 00 00 00 11 28 00 00 00 00 00 00
There is one clear pattern: the size of the malloc’ed memory chunk is always found in the preceding 0x10 bytes. For instance, the first malloc call is malloc’ing 1024
(0x0400
) bytes and we can find 11 04 00 00 00 00 00 00
in the preceding 0x10
bytes. Those last bytes represent the number 0x 00 00 00 00 00 00 04 11
= 0x400 (1024) + 0x10 (the block size preceding those 1024
bytes + 1
(we’ll talk about this “+1” later in this chapter).
If we look at each 0x10
bytes preceding the addresses returned by malloc, they all contain the size of the chunk of memory asked to malloc + 0x10
+ 1
.
At this point, given what we said and saw earlier, we can probably guess that those 0x10 bytes are a sort of data structure used by malloc (and free) to deal with the heap. And indeed, even though we don’t understand everything yet, we can already use this data structure to go from one malloc’ed chunk of memory to the other (6-main.c
) as long as we have the address of the beginning of the heap (and as long as we have never called free):
根据之前所说和看到的,可以猜测这些0x10字节是malloc(和free)用来处理堆的一种数据结构。事实上,即使还不完全理解,只要有堆开始的地址(并且从未调用过free),就可以使用这种数据结构从一个malloc的内存块转移到另一个('6-main.c'):
6-main.c: use this 0x10 bytes to go from one malloc’ed chunk of memory to the other
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void pmem(void *p, unsigned int bytes)
{
unsigned char *ptr;
unsigned int i;
ptr = (unsigned char *)p;
for (i = 0; i < bytes; i++)
{
if (i != 0)
{
printf(" ");
}
printf("%02x", *(ptr + i));
}
printf("\n");
}
/**
* main - using the 0x10 bytes to jump to next malloc'ed chunks
*/
int main(void)
{
void *p;
int i;
void *heap_start;
size_t size_of_the_block;
heap_start = sbrk(0);
write(1, "START\n", 6);
for (i = 0; i < 10; i++)
{
p = malloc(1024 * (i + 1));
*((int *)p) = i;
printf("%p: [%i]\n", p, i);
}
p = heap_start;
for (i = 0; i < 10; i++)
{
pmem(p, 0x10);
size_of_the_block = *((size_t *)((char *)p + 8)) - 1; // 通过前面pmem打印内容及分析 `+8` 后才能get alloc size, `-1` later in this chapter
printf("%p: [%i] - size = %lu\n",
(void *)((char *)p + 0x10),
*((int *)((char *)p + 0x10)),
size_of_the_block);
p = (void *)((char *)p + size_of_the_block);
}
write(1, "END\n", 4);
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 6-main.c -o 6
$ ./6
START
0x9e6010: [0]
0x9e6420: [1]
0x9e6c30: [2]
0x9e7840: [3]
0x9e8850: [4]
0x9e9c60: [5]
0x9eb470: [6]
0x9ed080: [7]
0x9ef090: [8]
0x9f14a0: [9]
00 00 00 00 00 00 00 00 11 04 00 00 00 00 00 00
0x9e6010: [0] - size = 1040
00 00 00 00 00 00 00 00 11 08 00 00 00 00 00 00
0x9e6420: [1] - size = 2064 # (2048+16)
00 00 00 00 00 00 00 00 11 0c 00 00 00 00 00 00
0x9e6c30: [2] - size = 3088 # (3072+16)
00 00 00 00 00 00 00 00 11 10 00 00 00 00 00 00
0x9e7840: [3] - size = 4112
00 00 00 00 00 00 00 00 11 14 00 00 00 00 00 00
0x9e8850: [4] - size = 5136
00 00 00 00 00 00 00 00 11 18 00 00 00 00 00 00
0x9e9c60: [5] - size = 6160
00 00 00 00 00 00 00 00 11 1c 00 00 00 00 00 00
0x9eb470: [6] - size = 7184
00 00 00 00 00 00 00 00 11 20 00 00 00 00 00 00
0x9ed080: [7] - size = 8208
00 00 00 00 00 00 00 00 11 24 00 00 00 00 00 00
0x9ef090: [8] - size = 9232
00 00 00 00 00 00 00 00 11 28 00 00 00 00 00 00
0x9f14a0: [9] - size = 10256
END
One of our open questions from the previous chapter is now answered: malloc is using 0x10
additional bytes for each malloc’ed memory block to store the size of the block. This data will actually be used by free to save it to a list of available blocks for future calls to malloc.
+++
But our study also raises a new question: what are the first 8 bytes of the 16 (0x10 in hexadecimal) bytes used for? It seems to always be zero. Is it just padding?
从后面的学习,这里均为zero原因是都未有过free
The first 8 bytes of the 16 (0x10 in hexadecimal) bytes
At this stage, we probably want to check the source code of malloc to confirm what we just found (malloc.c
from the glibc).
RTFSC 😃
1055 /*
1056 malloc_chunk details:
1057
1058 (The following includes lightly edited explanations by Colin Plumb.)
1059
1060 Chunks of memory are maintained using a `boundary tag' method as
1061 described in e.g., Knuth or Standish. (See the paper by Paul
1062 Wilson ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps for a
1063 survey of such techniques.)
1064 Sizes of free chunks are stored both in the front of each chunk and at the end.
1065 This makes consolidating fragmented chunks into bigger chunks very fast. The
1066 size fields also hold bits representing whether chunks are free or in use.
1067
1068
1069 An allocated chunk looks like this:
1070
1071
1072 chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1073 | Size of previous chunk, if unallocated (P clear) |
1074 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1075 | Size of chunk, in bytes |A|M|P|
1076 mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1077 | User data starts here... .
1078 . .
1079 . (malloc_usable_size() bytes) .
1080 . |
1081 nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1082 | (size of chunk, but used for application data) |
1083 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1084 | Size of next chunk, in bytes |A|0|1|
1085 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1086
1087 Where "chunk" is the front of the chunk for the purpose of most of
1088 the malloc code, but "mem" is the pointer that is returned to the
1089 user. "Nextchunk" is the beginning of the next contiguous chunk.
-> We were correct \o/. Right before the address returned by malloc to the user, we have two variables:
- Size of previous chunk, if unallocated: we never free’d any chunks so that is why it was always 0 程序中未有过free,所以always 0
- Size of chunk, in bytes
Let’s free some chunks to confirm that the first 8 bytes are used the way the source code describes it (7-main.c
):
7-main.c: when the previous chunk has been free’d, the malloc chunk’s first 8 bytes contain the size of the previous unallocated chunk
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void pmem(void *p, unsigned int bytes)
{
unsigned char *ptr;
unsigned int i;
ptr = (unsigned char *)p;
for (i = 0; i < bytes; i++)
{
if (i != 0)
{
printf(" ");
}
printf("%02x", *(ptr + i));
}
printf("\n");
}
int main(void)
{
void *p;
int i;
size_t size_of_the_chunk;
size_t size_of_the_previous_chunk;
void *chunks[10];
for (i = 0; i < 10; i++)
{
p = malloc(1024 * (i + 1));
chunks[i] = (void *)((char *)p - 0x10);
printf("%p\n", p);
}
free((char *)(chunks[3]) + 0x10);
free((char *)(chunks[7]) + 0x10);
for (i = 0; i < 10; i++)
{
p = chunks[i];
printf("chunks[%d]: ", i);
pmem(p, 0x10);
size_of_the_chunk = *((size_t *)((char *)p + 8)) - 1;
size_of_the_previous_chunk = *((size_t *)((char *)p));
printf("chunks[%d]: %p, size = %li, prev = %li\n",
i, p, size_of_the_chunk, size_of_the_previous_chunk);
}
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 7-main.c -o 7
$ ./7
0x1536010
0x1536420
0x1536c30
0x1537840
0x1538850
0x1539c60
0x153b470
0x153d080
0x153f090
0x15414a0
chunks[0]: 00 00 00 00 00 00 00 00 11 04 00 00 00 00 00 00
chunks[0]: 0x1536000, size = 1040, prev = 0
chunks[1]: 00 00 00 00 00 00 00 00 11 08 00 00 00 00 00 00
chunks[1]: 0x1536410, size = 2064, prev = 0
chunks[2]: 00 00 00 00 00 00 00 00 11 0c 00 00 00 00 00 00
chunks[2]: 0x1536c20, size = 3088, prev = 0
chunks[3]: 00 00 00 00 00 00 00 00 11 10 00 00 00 00 00 00
chunks[3]: 0x1537830, size = 4112, prev = 0
chunks[4]: 10 10 00 00 00 00 00 00 10 14 00 00 00 00 00 00
chunks[4]: 0x1538840, size = 5135, prev = 4112
chunks[5]: 00 00 00 00 00 00 00 00 11 18 00 00 00 00 00 00
chunks[5]: 0x1539c50, size = 6160, prev = 0
chunks[6]: 00 00 00 00 00 00 00 00 11 1c 00 00 00 00 00 00
chunks[6]: 0x153b460, size = 7184, prev = 0
chunks[7]: 00 00 00 00 00 00 00 00 11 20 00 00 00 00 00 00
chunks[7]: 0x153d070, size = 8208, prev = 0
chunks[8]: 10 20 00 00 00 00 00 00 10 24 00 00 00 00 00 00
chunks[8]: 0x153f080, size = 9231, prev = 8208
chunks[9]: 00 00 00 00 00 00 00 00 11 28 00 00 00 00 00 00
chunks[9]: 0x1541490, size = 10256, prev = 0
As we can see from the above listing, when the previous chunk has been free’d, the malloc chunk’s first 8 bytes contain the size of the previous unallocated chunk. So the correct representation of a malloc chunk is the following:
Also, it seems that the first bit of the next 8 bytes (containing the size of the current chunk) serves as a flag to check if the previous chunk is used (1
) or not (0
). So the correct updated version of our program should be written this way (8-main.c
):
8-main.c: update version of 7-main.c, 补充打印P flag
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void pmem(void *p, unsigned int bytes)
{
unsigned char *ptr;
unsigned int i;
ptr = (unsigned char *)p;
for (i = 0; i < bytes; i++) {
if (i != 0) {
printf(" ");
}
printf("%02x", *(ptr + i));
}
printf("\n");
}
int main(void)
{
void *p;
int i;
size_t size_of_the_chunk;
size_t size_of_the_previous_chunk;
void *chunks[10];
char prev_used;
for (i = 0; i < 10; i++) {
p = malloc(1024 * (i + 1));
chunks[i] = (void *)((char *)p - 0x10);
}
free((char *)(chunks[3]) + 0x10);
free((char *)(chunks[7]) + 0x10);
for (i = 0; i < 10; i++) {
p = chunks[i];
printf("chunks[%d]: ", i);
pmem(p, 0x10);
size_of_the_chunk = *((size_t *)((char *)p + 8));
prev_used = size_of_the_chunk & 1;
size_of_the_chunk -= prev_used;
size_of_the_previous_chunk = *((size_t *)((char *)p));
printf("chunks[%d]: %p, size = %li, prev (%s) = %li\n",
i, p, size_of_the_chunk,
(prev_used? "allocated": "unallocated"), size_of_the_previous_chunk);
}
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 8-main.c -o 8
$ ./8
chunks[0]: 00 00 00 00 00 00 00 00 11 04 00 00 00 00 00 00
chunks[0]: 0x1031000, size = 1040, prev (allocated) = 0
chunks[1]: 00 00 00 00 00 00 00 00 11 08 00 00 00 00 00 00
chunks[1]: 0x1031410, size = 2064, prev (allocated) = 0
chunks[2]: 00 00 00 00 00 00 00 00 11 0c 00 00 00 00 00 00
chunks[2]: 0x1031c20, size = 3088, prev (allocated) = 0
chunks[3]: 00 00 00 00 00 00 00 00 11 10 00 00 00 00 00 00
chunks[3]: 0x1032830, size = 4112, prev (allocated) = 0
chunks[4]: 10 10 00 00 00 00 00 00 10 14 00 00 00 00 00 00
chunks[4]: 0x1033840, size = 5136, prev (unallocated) = 4112
chunks[5]: 00 00 00 00 00 00 00 00 11 18 00 00 00 00 00 00
chunks[5]: 0x1034c50, size = 6160, prev (allocated) = 0
chunks[6]: 00 00 00 00 00 00 00 00 11 1c 00 00 00 00 00 00
chunks[6]: 0x1036460, size = 7184, prev (allocated) = 0
chunks[7]: 00 00 00 00 00 00 00 00 11 20 00 00 00 00 00 00
chunks[7]: 0x1038070, size = 8208, prev (allocated) = 0
chunks[8]: 10 20 00 00 00 00 00 00 10 24 00 00 00 00 00 00
chunks[8]: 0x103a080, size = 9232, prev (unallocated) = 8208
chunks[9]: 00 00 00 00 00 00 00 00 11 28 00 00 00 00 00 00
chunks[9]: 0x103c490, size = 10256, prev (allocated) = 0
为什么需要 the first 8 bytes of the 0x10 bytes ?
chunk在被释放时,肯定希望和前后空闲的chunk合并成一个更大的chunk,那怎么知道前后的chunk是空闲的呢,它们的大小又是多大呢?
1、判断前一个chunk是否空闲
- 当前面一个chunk是空闲时,当前chunk p=0,且开始8个字节填的是前一个chunk的长度(persize),这样就可以知道前一个chunk的开始地址,然后就可以进行合并。
2、判断下一个chunk是否空闲
- 要查找下一个chunk状态和大小,则直接通过本chunk的地址+本chunk的长度就可以直接指定到下一个chunk地址
- 如何判断下一个chunk是否空闲?,显然,要通过
下一个chunk的下一个chunk的P
来判断,其值为0则表示可以合并
Is the heap actually growing upwards?
The last question left unanswered is: “Is the heap actually growing upwards?”. From the brk
man page, it seems so:
DESCRIPTION
brk() and sbrk() change the location of the program break, which defines the end of the
process's data segment (i.e., the program break is the first location after the end of
the uninitialized data segment). Increasing the program break has the effect of allocat‐
ing memory to the process; decreasing the break deallocates memory.
Let’s check! (9-main.c
)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int i;
write(1, "START\n", 6);
malloc(1);
getchar();
write(1, "LOOP\n", 5);
for (i = 0; i < 0x25000 / 1024; i++) {
malloc(1024);
}
write(1, "END\n", 4);
getchar();
return (EXIT_SUCCESS);
}
Now let’s confirm this assumption with strace:
$ strace ./9
execve("./9", ["./9"], [/* 61 vars */]) = 0
...
write(1, "START\n", 6START
) = 6
brk(0) = 0x1fd8000
brk(0x1ff9000) = 0x1ff9000
...
write(1, "LOOP\n", 5LOOP
) = 5
brk(0x201a000) = 0x201a000
write(1, "END\n", 4END
) = 4
...
clearly, malloc made only two calls to brk
to increase the allocated space on the heap. And the second call is using a higher memory address argument (0x201a000
> 0x1ff9000
). The second syscall was triggered when the space on the heap was too small to host all the malloc calls.
Let’s double check with /proc
.
$ gcc -Wall -Wextra -pedantic -Werror 9-main.c -o 9
$ ./9
START
$ ps aux | grep \ \./9$
julien 7972 0.0 0.0 4332 684 pts/9 S+ 19:08 0:00 ./9
$ cat /proc/7972/maps
...
00901000-00922000 rw-p 00000000 00:00 0 [heap]
...
-> 00901000-00922000 rw-p 00000000 00:00 0 [heap]
Let’s hit Enter and look at the [heap] again:
LOOP
END
$ cat /proc/7972/maps
...
00901000-00943000 rw-p 00000000 00:00 0 [heap]
...
-> 00901000-00943000 rw-p 00000000 00:00 0 [heap]
The beginning of the heap is still the same, but the size has increased upwards from 00922000 to 00943000
Address Space Layout Randomisation (ASLR)
You may have noticed something “strange” in the /proc/pid/maps
listing above, that we want to study:
The program break is the address of the first location beyond the current end of the data region – so the address of the first location beyond the executable in the virtual memory. As a consequence, the heap should start right after the end of the executable in memory. As you can see in all above listing, it is NOT the case. The only thing that is true is that the heap is always the next memory region after the executable, which makes sense since the heap is actually part of the data segment of the executable itself.
Also, if we look even closer, the memory gap size between the executable and the heap is never the same:
-
Format of the following lines: [PID of the above maps listings]: address of the beginning of the [heap] – address of the end of the executable = memory gap size
+ \[3718\]: 01195000 – 00602000 = b93000 + \[3834\]: 024d6000 – 00602000 = 1ed4000 + \[4014\]: 00e70000 – 00602000 = 86e000 + \[4172\]: 01314000 – 00602000 = d12000 + \[7972\]: 00901000 – 00602000 = 2ff000
It seems that this gap size is random, and indeed, it is. If we look at the ELF binary loader source code (fs/binfmt_elf.c
) we can find this:
-
fs/binfmt_elf.c
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) { current->mm->brk = current->mm->start_brk = arch_randomize_brk(current->mm); #ifdef compat_brk_randomized current->brk_randomized = 1; #endif }
where
current->mm->brk
is the address of the program break. Thearch_randomize_brk
function can be found in thearch/x86/kernel/process.c
file:unsigned long arch_randomize_brk(struct mm_struct *mm) { unsigned long range_end = mm->brk + 0x02000000; return randomize_range(mm->brk, range_end, 0) ? : mm->brk; }
The
randomize_range
returns a start address such that:[...... <range> .....] start end
As a result, the offset between the data section of the executable and the program break initial position when the process runs can have a size of anywhere between 0
and 0x02000000
. This randomization is known as Address Space Layout Randomisation (ASLR). ASLR is a computer security technique involved in preventing exploitation of memory corruption vulnerabilities. In order to prevent an attacker from jumping to, for example, a particular exploited function in memory, ASLR randomly arranges the address space positions of key data areas of a process, including the positions of the heap and the stack.
The updated VM diagram
With all the above in mind, we can now update our VM diagram:
malloc(0)
Did you ever wonder what was happening when we call malloc with a size of 0
? Let’s check! (10-main.c
)
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void pmem(void *p, unsigned int bytes)
{
unsigned char *ptr;
unsigned int i;
ptr = (unsigned char *)p;
for (i = 0; i < bytes; i++)
{
if (i != 0)
{
printf(" ");
}
printf("%02x", *(ptr + i));
}
printf("\n");
}
int main(void)
{
void *p;
size_t size_of_the_chunk;
char prev_used;
p = malloc(0);
printf("%p\n", p);
pmem((char *)p - 0x10, 0x10);
size_of_the_chunk = *((size_t *)((char *)p - 8));
prev_used = size_of_the_chunk & 1;
size_of_the_chunk -= prev_used;
printf("chunk size = %li bytes\n", size_of_the_chunk);
return (EXIT_SUCCESS);
}
$ gcc -Wall -Wextra -pedantic -Werror 10-main.c -o 10
$ ./10
0xd08010
00 00 00 00 00 00 00 00 21 00 00 00 00 00 00 00
chunk size = 32 bytes
-> malloc(0) is actually using 32 bytes, including the first 0x10
bytes.
Again, note that this will not always be the case. From the man page (man malloc
):
NULL may also be returned by a successful call to malloc() with a size of zero # NULL可能被return 即使malloc(0) successful
Outro
We have learned a couple of things about malloc and the heap. But there is actually more than brk
and sbrk
. You can try malloc’ing a big chunk of memory, strace
it, and look at /proc
to learn more before we cover it in a next chapter
Also, studying how free works in coordination with malloc is something we haven’t covered yet. If you want to look at it, you will find part of the answer to why the minimum chunk size is 32 (when we ask malloc for 0 bytes) vs 16 (0x10 in hexadecimal) or 0.
As usual, to be continued! Let me know if you have something you would like me to cover in the next chapter.
Files
This repo contains the source code (naive_malloc.c
, version.c
& “X-main.c` files) for programs created in this tutorial.
本文来自博客园,作者:LiYanbin,转载请注明原文链接:https://www.cnblogs.com/stellar-liyanbin/p/18861386