On many microcontroller systems, compilers and linkers allocate data from the bottom and/or top of RAM; unused RAM will usually be in a contiguous area, and it's generally possible to convince the linker to make the addresses of the start and end of that area available to the programmer (e.g. one could ask the linker to place a one-byte variable as the very last thing before the free area, and another as the very first thing following; the addresses of those variables would delineate the free space).
Although a lot of system offer a
malloc() function, it's more complicated than necessary in a lot of scenarios. For example, if one will need to allocate objects based upon information received at run-time, but objects will always be freed in the reverse order of allocation, one may simply keep a pointer to "next available space". To allocate an object, copy that pointer add the size of the new object to it, and return the copied pointer. To free an object (and everything allocated after it), simply set the "next available space" pointer to the start of that object.
Some scenarios are a little more complex than would be allowed with that approach, but still not need a full "malloc/free" approach. For example, in some scenarios, one may have two groups of objects which obey the last-in-first-out rule among themselves, but objects in one group may outlive those in the other. To deal with that situation, use a "next available space" pointer which starts at the bottom of free space, and a "previous available space" pointer which starts at the top. Allocate things in the first group as before. For those in the second group, subtract the size of the new object from the "previous available space" pointer and return the result. To free an object in the second group (and all second-group objects allocated after it), add the size of the object to its address and store the result to the "previous available space" pointer.
free are designed so that it's possible for objects in the middle of memory to be freed, while objects before and after remain valid. While this can at times be useful, it can also lead to memory fragmentation. If one's objects will not fit with a last-in-first-out model, it may be a good idea to make objects relocatable. For example, one might hold a queue of variable-sized objects which get stored consecutively in memory as long as there's room. When memory gets full (or at some convenient time before that), one would copy the first object that hadn't yet been read to the start of the queue, the next object that hadn't been read immediately following it, etc. until all objects had been copied, consolidating the free space that had been at the start of the queue, to the end. One must be careful when moving any data to ensure that pointers to that data are updated accordingly, but that's often not too difficult. Moving memory around may take more CPU cycles than managing discontinuous areas of free space, but the behavior of the moving-memory approach may be more predictable.