pete > courses > CS 315 Fall 26 > lecture 07: malloc


Lecture 07: malloc

Goals


(following notes are necessarily short because much of the class time was discussion that led to the following conclusions…)


question: is malloc a system call?

some intuition both ways:

(the latter intuition is similar to the reasons for not using read(2) and write(2) directly)


how might you find out for sure?

here’s a program we can use to implement the latter strategy: simple-malloc.c

and a Makefile that includes a handy recipe to run strace for us

$ make simple-malloc.strace
gcc -Wall -pedantic -g -o simple-malloc simple-malloc.c
strace -o simple-malloc.strace ./simple-malloc

(recall: normally, strace will send its record of syscalls to the terminal; the -o option is used to send that record to the named file)

now, if we look in simple-malloc.strace, we see all the boilerplate we’ve seen before

and, at the very end, in the collection of syscalls that are invoked as a result of the code in our main function, we see

brk(NULL)                               = 0x5631192ad000
brk(0x5631192ce000)                     = 0x5631192ce000

huh. I wonder what the brk syscall does.

to the manpage!

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.

these words may all make sense, but it may be difficult to see how they relate to the Heap (which, recall, is the name for the region of memory from which malloc allocates memory)

to put it into context, let us return to (a simplified version of) the diagram that shows the memory layout of a process:

0x7ff..fff  +---------------------------+
            |                           |
            |           stack           |
            |             |             |
            |             v             |
            +---------------------------+
            |                           |
            |                           |
            |                           |
            |                           |
            |                           |
            |                           |
            |                           |
            +---------------------------+
            |            ^              |
            |            |              |
            |           heap            |
            |                           |
            |                           |
            +---------------------------+
            |                           |
            |       instructions        |
            |                           |
            +---------------------------+
            |                           |
            |                           |
            |                           |
            |                           |
            |                           |
0x000..000  +---------------------------+

the stack grows down, the heap grows up, the instructions are more or less static

the upper border of the Heap is what the manage is referring to as the program break

and the brk and sbrk system calls are used to move it up (make it bigger) and down (make it smaller) as necessary

brk allows you to specify exactly the address at which you want the heap to end

sbrk allows you to specify the desired change in the size of the heap (positive or negative)

in other words: brk is absolute, sbrk is relative


so if we ask for memory with malloc, that function will turn around and call brk or sbrk to make the heap bigger and thus make room for the new chunk of memory requested

this may feel weird: what’s in the memory beforehand? what about malloc "gives" a certain chunk of memory to the caller?

the heap is a bit strange: it’s not organized at all, even though you might think it would be

it’s just a region of memory out of which chunks are carved and returned by malloc

there is no notion of boundaries between those chunks; malloc just makes sure that, when you call it, it returns the address of the beginning of a chunk of memory that hasn’t been returned by any other call to malloc

(setting aside for the time being the behavior of free, which explicitly marks an allocation as no longer being used)


so how might we go about implementing malloc using brk and/or sbrk ?

if the user calls malloc(12) is it enough to just call sbrk(12) ?

no, for (at least) two reasons:

the latter point implies that we need some bookkeeping information in the heap to keep track of all the allocations made so that we can do the right thing when free is called

it makes sense that we would have to store some information about every allocation, because every allocation might be different and free’ing each would need to be handled differently


where to store this bookkeeping information?

not the stack: if the frames get popped, the information is lost

not the instruction section: because heap metadata is data

logical choice is within the heap itself

conclusion: the heap consists of both the chunks of memory being allocated with malloc as well as the metadata about those chunks that allow malloc and free to manage them


what sort of data structure would we use to keep track of all this information about the allocations?

an array kind of makes sense, because we expect to have a bunch of presumably-structs, one for each allocation

except arrays are fixed-size: we’d have to set aside a specific amount of space at the beginning and hope it’s enough to manage all the allocations

additionally, if a program makes very few allocations, much of the memory consumed by the array would go unused, which is wasteful

is there something similar to an array that can grow and shrink dynamically?

why yes, yes there is: a linked list


modern heap implementations are super complex (I’ve linked some really good articles in the description for assignment 3) but most of them do indeed use a linked list (or several) to keep track of allocations

one way (which, hint hint, you could implement for assignment 3) is to set up the heap such it’s a sequence of "struct A, allocation A, struct B, allocation B, struct C, allocation C, etc"

whereby the struct describing a particular allocation immediately precedes the allocated memory itself


how about free ?

it could just mark an allocation as no longer being used

but then what if malloc is called again and the now-unused chunk is an appropriate size to be allocated?

this is something you’ll have to figure out in the assignment

you’re going to get very comfortable with pointers and gdb on this one


as we’ve beaten into the ground many times, we can think of memory as a massive array

where each element of the array holds a single byte of data (and is consequently referred to as byte-addressable memory)

deep inside the hardware, there are physical devices that store these bits and connected physical devices that transfer these bits from memory to the CPU for to make computation

transferring a single byte at a time is horrendously inefficient, especially when we consider that many of the values we work with are larger (32-bit integers, 64-bit floats, variable-length strings, etc)

therefore, a straightforward optimization that pretty much all hardware uses these days is to transfer more than a single byte at a time

the wiring between memory (or cache, to be absolutely precise) and CPU is often set up such that the path is the same width as (or an integer multiple of) the word size

recall that the word size is a parameter of the hardware design, and is the number of bits used to store a memory address

so in modern machines, the wiring between memory (cache) and CPU allows us to transfer 8 bytes (64 bits) of data at a time


there’s actually a subtlety here: we can’t transfer just any contiguous group of 8 bytes at once

in effect, memory is broken up into consecutive 8-byte chunks: the first chunk consists of bytes 0 through 7; the second of bytes 8 through 15, etc

therefore, if the 8-byte value upon which I wish to compute is stored in addresses 256-263, I can transfer it all at once

because the starting address is evenly divisible by 8

rephrased, because the starting address mod 8 is 0

such a value is said to be aligned


an unaligned value is one that spans multiple 8-byte chunks

for instance, if my 8-byte value starts on address 100 (and therefore lives in address 100-107), then its first half will be in the 96-103 chunk and its second half will be in the 104-111 chunk

reading this value from memory is much less efficient: two memory operations, then some shifting and combining


as a result, for efficiency, it behooves programs to operate on aligned values whenever they can

it turns out that some software even expects memory returned by malloc to be aligned

so if your implementation returns addresses that are not aligned, weird behavior could ensue, quite often leading to a crash

in fact, the libc version of malloc returns addresses that are always aligned to 16-byte boundaries

when I modified my implementation to do the same, the crashes went away

take-home message: make sure your implementation of malloc returns addresses that are evenly divisible by 16 (ie, address mod 16 is 0)

Last modified: