Lecture 16: Dynamic Memory, continued (by Trek Palmer) ======================================= Dynamic Memory and Process Layout ---------------------------------- Throughout the course, you have been introduced piecemeal to all the separate structures that actually comprise the state of an executing program. Those pieces are: 1) Text: This is the actual code. Stuck in memory somewhere and pointed at by the PC 2) PC: the program counter, points at the currently executing instruction 3) Registers: the system registers, they contain values currently being computed on and temporaries necessary for future operations 4) Static Data: These are the constant sized data elements that the program reads and writes. All values in the .data section are of a fixed size, but may be modified by the program. Anything whose size can be ascertained at compilation/assembly time can be placed in the static data section 5) The Stack: The stack holds the state for function calls, and is used to preserve function-local data and the return address (link register) across potentially destructive function calls. Note that the PC and the registers are the only part of this program state that are actually on the CPU. Everything else is in main memory. Looks like we've got everything we need here, right? Wrong. There is one crucial missing part. The heap. The heap ------------- To understand the heap will require some un-Java-ish thinking. In Java, the virtual machine handles all your memory needs for you. You need a new object (that is to say, you need a bit more memory), the VM hands it to you when you call new. The VM goes through all kinds of trouble to preserve the illusion of infinite memory. When it runs out of actual memory, it cleans up all the objects your program can no longer access (garbage). This process is known as garbage collection or automatic memory management. As programmers, you should like this, because it means you have to do less work. But garbage collection is a complex process that requires substantial infrastructure to implement. So substantial, in fact, that with few exceptions (the lisp machines, and some new java architectures), GC is never implemented in hardware. So, in most systems, if you don't have GC available you have to do what is known as explicit memory management. You have to manually go out there, grab a hunk of memory, and then remember to release it when you're done with it. It's this last part, in particular, that makes explicit memory management difficult. So, this area of grabbable memory is commonly referred to as the heap. In reality, the heap is just another portion of memory, and it's up to your program to manage it. You can visualize the heap as a large contiguous section of memory. You can write values into it and read values out of it. What those values mean, and more importantly where they begin and end, are all up to interpretation by your program. This is another difference with Java. In Java, a String is always going to be a reference pointing to a String object. The VM and the compiler ensure that this is the case. In lower-level languages, there are often no such guarantees. A memory address is a memory address. The hardware has no concept of Strings, HashMaps, or whatever. You load a value from memory, compute on it, and then store it back. All the hardware knows is bytes, halfs, and words. Everything else is negotiable. The reason for the heap is that not all data is statically known. Imagine a linked-list implementation. You don't know how many elements someone's going to put in your list, so you can't pre-allocate a maximum number of nodes because it could be exceeded. Therefore you can't just grab a chunk of memory in .data and use it. You need something more dynamic. You need to be able to grab chunks of memory of different sizes and one at a time. This is what the heap is for. In terms of assembly, what you'll do is ask the OS for a chunk of memory and you'll allocate your dynamic data into it. You run out of memory, don't worry, just ask the OS for more! In this case, the OS is performing a similar role to the JVM. But the OS will NOT clean up your memory. You have to tell it when you're done with something. And when you hand the memory back to the OS, it's gone. There's no way to get at values that were in that memory after you've returned it, so you better be sure you're done with it before you hand it back. The procedure for using dynamic memory in assembly is this: 1) Figure out how much memory you're going to need for this allocation 2) Ask the OS for some more memory (which may be handed out in chunks of a different size) 3) Allocate your data into the new memory from the OS 4) Compute on it some 5) Return it to the OS (or exit, which will do the same thing) Allocating in the heap ----------------------- First, a Java-like example to show you how things usually get stuck in the heap. If you have a pointer (address) to the beginning of the heap, and the heap is empty it looks like: heap pointer ---+ | v the heap +----------------------------------------------------------------+ | | +----------------------------------------------------------------+ now, if we were to allocate something in the heap, like say by calling: String str = new String("some data to stick in memory"); So we stick the data representing this String into the heap, starting at the heap pointer, and then we move (the technical term is "bump") the pointer to the end of the String so we know where to place the next guy. heap pointer -----------+ | v the heap +-------+--------------------------------------------------------+ | str | | +-------+--------------------------------------------------------+ Now, what if we allocate something else, like: Integer i = new Integer(0xDEADBEEF); We do exactly the same thing, we figure out how big i is, and write out the appropriate number of bytes to the next available slot in the heap. heap pointer ---------------+ | v the heap +-------+---+----------------------------------------------------+ | str | i | | +-------+---+----------------------------------------------------+ Seems simple, doesn't it? Well, trust me, it isn't. Memory allocation gets complicated fast. We haven't dealt with de-allocation, moving objects, or any of the other gems that make a true memory allocator the impressive engineering feat that it is. We haven't even dealt with getting references to the data you just allocated! In order to figure out where things are, you need to copy the value of the heap pointer before you adjust it, then that copied value will point to the beginning of your newly allocated object. Note also that there's no protection here. If I forget to check for the string length and I write out data past the end of str, there's no way for the system to know I've just made a programming error. In Java, there are no pointers (that is to say, addresses), so there's no way to write outside of an object, but at the assembly level, an address is an address. If str is 80 bytes long, but if you accidentally add 84 to the pointer, the CPU doesn't care. It'll dutifully perform your operations and overwrite values in i, which will probably cause your program to misbehave mysteriously. The lesson here is always be sure of your lengths/offsets, because the hardware will NOT check any of that for you! Actually using dynamic memory ----------------------------- Because the operating system handles many of the details of memory management, the way your programs will get more dynamic memory is through a software interrupt. Now, the virtual memory system that the OS deals with understands memory as pages. A page is basically an organizational unit of memory normally a power of two in size. In the case of rolecks, the pages are 1K in size. So you get blocks of memory from the OS 1K at a time. swi #h00F0000F, will get a new page from the OS and will put the starting address of the page in R0. swi #h00f0000f ;initializing heap mov R5, #0 mov R6, #h41 ;'A' mov R7, #1 mov R7, R7, LSL #10 ;R7 = 1024 = 1K = size of page loop: strb R6, [R0, R5] add R5, R5, #1 cmp R5, R7 ;have we run off the page? blt loop swi #h00f0000f ;getting new page mov R5, #0 mov R6, #h42 ;'B' loop2: strb R6, [R0, R5] add R5, R5, #1 cmp R5, R7 ;have we run off the page? blt loop2 swi #h00900001 This code will allocate a page, fill it with 'A', allocate another page and fill it with 'B'.