Please note I made a mistake in register naming.
Memory is the fundamental thing we work with in programming. All a CPU or GPU does is take in some values from memory, modify them, and put them back out to memory.
Anything we want to do wether it's drawing images on the screen, creating audio, 3d graphics, etc.. it's all about putting stuff in memory where other things(aka hardware) could read it and turn it into proper electric signals to create pixels on a screen, audio out of the speaker,printed model, etc....
Programming is fundamentally understanding what is happening with the cpu and the memory.
The primary question is, What is the cpu doing to change memory and how
This thinking should be the basis for all your decisions when it comes to programming
All your design decisions, all of you optimazation decisions should be based on this premise.
No matter what programming language you use, at the end of the day, you are instructing the cpu to modify memory.
Higher level abstractions can help us, how every we cannot lose sight of the fact that we are manipulating memory. When we make design decisions about our program it should be in service of this, not in service of the abstraction. If the decisions we make serve only the abstractions it will lead to our code being buggy and slow.
In c/c++ memory is a first class citizen. In c/c++ you can always get an answer to the question. "Where is this value ?"
1 char unsigned Test;
The name Test in line 1, refers to a byte memory that holds a numeric value, that will be treated as a positive number. That byte has an Address. In c/c++ we can always find out what that address is.
Addreses are also numeric values that can be stored in memory. When we store an address in memory we call it a pointer.
In line 2 below,by using the * symbol, we ask the compiler to give us some memory for storing an addresss.
1 char unsigned Test;
2 char unsigned *TestPointer;
3
4 TestPointer = &Test;
By using the & symbol we can find out the address of any variable. Since TestPointer is a pointer variable, that is a variable that holds addresses it can hold the address of Test, like in line 4
the address that is stored in a pointer variable can be manipulated just like any other number stored in memory.
with pointer variables you can
in any variable declaration if you use the * symbol you are telling the compiler Give me some memory where I can store an address that points to the TYPE specified prior to the *
int *NumberPointer; // address of a 4 byte value
char *BytePointer; // address of a 1 byte value
long *BigNumPointer; // address of a 8 byte value
In 32 bit machines addresses are 4 byte values
In 64 bit machines addresses are 8 byte values
It doesn't matter where the * is actually aligned it just still means the same thing
int* NumberPointer; // address of a 4 byte value
char * BytePointer; // address of a 1 byte value
long *BigNumPointer; // address of a 8 byte value
The & symbol placed in front of any variable resolves to the address of that variable, that is the location in memory where that variable is.
If the TYPE of variable is wider that one byte, the address that is returned using the & symbol specifies the first byte (the earliest address) of the variable.
An address is just a number. It is literally the number of bytes from the bottom of memory, The very first byte of memory, all the way up to where the compiler decided to store your variable. It is like a street address, but all of memory is one street.
On linux you can view the layout of the memory using
this command $ cat /proc/$(pgrep
In the old days(up until the late 80's), the address would actually be reffering to physical memory on the machine. But with modern OS, memory has been virtualized. So the numbers that a process(A running program) see's as it's address space, is not the address of actual physical memory. THE OS MAPS the addresses that a process sees to the actual physical memory behind the scenes. So a user process doesn't know the addresses that is actually physically using. But conceptually the virtual address space that a running process see's is exactly the same. It's the addresses on a single street of memory
virtual memory allows the physical memory to be shared amongs many programs without them stepping on each others data and code.It allows the Kernel(OS) to keep each processes memory sandboxed and the physical memory under exclusive control of the OS. For the time being When we are programming we can think the address space of our program as physical memory. But ulitmately we should understand the mechanisms used in mapping virtual to physical memory so we can address potential performance issues that may arise
Note: the * has a different meaning when not in a variable declaration
If you set Test = 6; and then dereference TestPointer by using the *, what will print out and why.
Lets revisit these declarations and assignments and view the chunk of memory that is being manipulated.
In gdb we change the value of any memory location while the program is running. See the next few slides to see how we do that. We are actually reaching in and manually setting switches when we do this. This is not a simulation
We are examing the top 30 bytes of the stack and viewing how a specific mem location chnages.
We can also write other sizes
We can view the memory in different formats
Local variables that are declared within a function have memory automatically allocated for them, on the stack, by the Operating system. You do not need to request that memomry explicitly. By declaring a variable within a function you are implicitly requesting memory for it on the stack. That memory is allocated automatically when the function is called.
Everything you write in code in C/C++ is built upon the notion that things go deeper and shallower in a very consistent way, using function calls.
It is like a russian nesting doll, You call a function from within another function. then you can return from that function to get back to the outer doll, or call another function from there going down to the doll inside, this pattern of calls and returns is called the call stack.
The area of memory known as the stack mirrors the function call stack. As you get deeper down the call stack, the memory stack gets bigger and bigger, return from each function, the memory stack shrinks and gets smaller
Nothing ever goes away from the middle of the stack
It always starts from 0 gets bigger smaller bigger bigger smaller as one continuous chunk.
This mirrors our call flow
stack only grows and shrinks from the end
Call | Grow |
Return | Shrink |
Call | Grow |
Call | Grow |
Call | Grow |
Return | Shrink |
Return | Shrink |
Lets trace what happens in memory, as we step thru this code
Before the first instruction upon entering main
$rsp = 0x7fffffffe698
$rbp = 0x0
We want to observe a large chunk of memory (100 bytes) during our walk thru so we subtract 100 from $rsp to get a lower bounds address
$rsp = 0x7fffffffe698
$rsp - 100 = 0x7fffffffe634
We will be observing 100 bytes from 0xe634 to 0xe698 for our entire walkthru.
This is the state of our memory window prior to executing any instructions
In gdb we can use the following command to set all the bytes in a block of memory to the same value. We are doing this so it will be easier to visualise changes as we step thru our program.
call memset(0x7fffffffe634,0x01,101)
This is how our block looks now.
We still haven't executed any instructions of our program. Here is the state of our stack registers.
$rsp = 0x7fffffffe698
$rbp = 0x0
When we first enter main the size of the stack is 0
We are veiwing 100 bytes that is in memory that will eventually be used for the stack, but when $rsp is set to the top of the stack, the stacks size is 0. Remember this stack grows downward.
The first thing that happens in stack memory EVERY TIME entering a function is the address stored in $rbp is pushed onto the stack using this instruction
push %rbp
In this case $rbp was set to 0x00 so the is the value pushed onto the stack is 0x00. All addresses are 8 bytes wide so all 8 bytes are set to 0
But notice when we executed the push instruction, $rsp was decrement by the number of bytes pushed!! The new address of $rsp is
old $rsp = 0x7fffffffe698
$rsp = 0x7fffffffe690
The next thing that happens on the stack EVERY TIME we enter a function is $rsp is copied over to $rbp using the following instruction.
mov %rsp,%rbp
For the moment $rsp and $rbp are pointing at the same address
0x7fffffffe690
the size of the stack is currently 8 bytes. The only thing that has happened was to push the value of $rbp onto the stack upon entering the function. If you recall we need to allocate some space on the stack for local variables declared in main. In main we as for a single byte of memory.
void main(void)
{
char Test = 0x10;
foo();
}
we need to grow the stack to make room for the variable TEST, so to grow the stack $rsp needs to get decremented. The compiler makes the decision about how many bytes should be decremented from $rsp to get the new address for $rsp. The space between $rbp and $rsp is called the STACK FRAME and that is how many bytes are being using on the stack for any particuliar function.
The compiler in this case decided to allocate 16 bytes for Main, even though we only need 1 byte for the char variable TEST. The instruction used was.
sub $0x10,%rsp
The current addresses now stored in $rbp and $rsp are
$rbp = 0x7fffffffe690
$rsp = 0x7fffffffe680
We have space on our current stack frame now. The compiler decided that char variable, Test would be stored at $rbp - 1. So the next instruction sets the value at that location.
The following shows the current values of $rsp, $rbp registers. It also shows the values stored in stack memory. Test is stored at $rbp -1 in green and The previous of $rbp is stored where $rbp is currently pointing
Now is the time to come to our first function call. Below is the disassembly.
When we use the call instruction it affects stack memory as well. An 8 byte address is pushed onto the stack and $rsp is decremented by the same amount. This address is the address of instruction to be executed in the calling function when we return from the called function
right after we execute the call instruction this is the state of $rsp and $rbp. $rbp was not affected by the call but $rsp was.
$rsp = 0x7fffffffe678
$rbp = 0x7fffffffe690
We have not exectud any instructions yet in foo(). The 8 byte value stored at the address on the stack pointed at by $rsp is the address of the instruction we are going to return to when we return from foo and go back to main.
The first thing we do EVERY TIME on the stack when entering a function is push $rbp. So $rsp gets decremented and the current value stored in $rbp is copied on to the stack.
$rbp = 0x7fffffffe690
The next thing we do on the stack EVERY TIME is copy $rsp into $rbp to start setting up the new stack frame for foo(). For a moment they are both the same address.
0x7fffffffe670
Room needs to be made for the local variables of foo. So $rsp is decremented 16 bytes, int FooVariable is located at $rbp - 4, its value is set. the foo() stack frame is 16 bytes
$rbp = 0x7fffffffe670
$rsp = 0x7fffffffe660
now we call bar() from foo()
in gdb if we call backtrace we can see the whole call stack as it stands now. it shows us where we are in our nesting dolls.
when we called bar. The address of the instruction to return to was pushed onto the stack so $rsp was decremented.
We are now in bar(), We are going to
The compiler decided not to decrement $rsp any further. This is likely for optimization purposes
In bar() prior to return $rsp and $rbp now both point to
0x7fffffffe650
We are now going to return from bar(). The first thing we do is issue the instruction
pop $rbp
We are about to issue our first retq instruction. We already set $rbp to point to the top of stack frame for foo(). Now we need to get back to the right instruction in foo and set $rsp to bottom of the foo() stack frame
Prior to the retq call
$rip = 0x55555555513a
$rsp = 0x7fffffffe658
$rbp = 0x7fffffffe670
$rsp is pointing at the return address that will be loaded into $rip by the retq instruction
$rsp will then be incremented by 8 bytes to shrink the stack
After the retq instruction
$rip = 0x555555555153
back in foo()
Then next instruction leaveq is a combination of two instructions
mov %rbp,%rsp // copy $rbp to $rsp
pop $rbp // restore previous value of $rbp
After the leaveq instruction
$rbp = 0x7fffffffe690
$rsp = 0x7fffffffe678
The last instruction left in foo() is retq, which will load the $rip register with the 8 byte value $rsp is pointing at and increment $rsp by 8 shrinking the stack.
When you call a function you push return address onto the stack, that stores the value and grows the stack by decrementing rsp
You then push $rbp, which stores the value of $rbp and grows the stack by decrementing $rsp.
You then copy rsp to rbp, so rbp is pointing at its own previous value
You then grow the stack by decrementing $rsp, to store local variables
To return from a function set rsp to rbp's value. this shrinks the stack, both pointing at prev rbp address.
You then pop $rbp which stores that address at rbp and shrinks the stack by decrementing rsp
rsp is now pointing at the address of the instruction to return to. When you call return the address at rsp gets copied to $rip and rsp gets incrementedx.
We are close to the end of our story. we are now in main().