The first time malloc called, the memory block is physically allocated by calling sbrk and added to the free list. At this point, the memory looks like this:
1 (base) freep ----------- ------- | 10 / 0 | | 1 | ----------- ------- 10 11 12 13 14 15 ------------------------------------------------------------- | 1 / 6 | | | | | | -------------------------------------------------------------
For simplicity, I numbered blocks of memory in terms of "units" rather than actual bytes. Here we assume that the global base variable lives at address 1 and that there is a block of 6 units assigned starting at address 10. In each module, the first number is the ptr value, and the second is the size value. In addition, freep contains the start address of the free block.
Thus, a free list starts with an empty base block, which then points to a block at address 10. This block contains 5 free units and points to a base block.
Suppose the current call to malloc requests 2 units. When we first introduce the for loop, prev == 1 and p == 10 :
1 (base) freep prev p ----------- ------- ------ ------ | 10 / 0 | | 1 | | 1 | | 10 | ----------- ------- ------ ------ 10 11 12 13 14 15 ------------------------------------------------------------- | 1 / 6 | | | | | | -------------------------------------------------------------
When added to the tail, the first statement p->s.size -= nunits; . This reduces the size in p by nunits :
1 (base) freep prev p ----------- ------- ------ ------ | 10 / 0 | | 1 | | 1 | | 10 | ----------- ------- ------ ------ 10 11 12 13 14 15 ------------------------------------------------------------- | 1 / 4 | | | | | | -------------------------------------------------------------
So, now the block with address 10 has a size of 4 units instead of 6. Next p += p->s.size; which adds the value of size to p :
1 (base) freep prev p ----------- ------- ------ ------ | 10 / 0 | | 1 | | 1 | | 14 | ----------- ------- ------ ------ 10 11 12 13 14 15 ------------------------------------------------------------- | 1 / 4 | | | | xx / xx | | -------------------------------------------------------------
Now p points to address 14. Currently, the ptr and size fields contain garbage, denoted here as "xx". Note that p not NULL. It points somewhere, but this memory has no meaningful data.
Now we run p->s.size = nunits; , which sets the size field in the module, which p points to:
1 (base) freep prev p ----------- ------- ------ ------ | 10 / 0 | | 1 | | 1 | | 14 | ----------- ------- ------ ------ 10 11 12 13 14 15 ------------------------------------------------------------- | 1 / 4 | | | | xx / 2 | | -------------------------------------------------------------
Then we have freep = prevp; , which sets a free pointer to the current prev value. There are no changes at this time. Then we return p+1 , which is equal to 15, the caller.
The end result is that the block pointed to by prev has not changed, but the one pointed to by prev->s.ptr is now less than the requested number of units, and address 14 is now the beginning of the allocated 2-unit memory block.
When address 15 is then passed to free , he looks at address 14 to see how big the allocated block is so that it can return these units to the free list.