Showing posts with label godbolt. Show all posts
Showing posts with label godbolt. Show all posts

Sunday, September 15, 2024

Wherein We Investigate A Function Prologue: Finally!

 

And here it is, finally: the majestic function prologue.


This is, in fact, part of a small C program and its Assembly version, as we can see here in Godbolt (again, use this - it's great).



Godbolt even distinguishes the different sections of our program (both in C and Assembly) in different colors, which is just a neat feature.


Assembly Prologue:

For starters, we'll represent the stack on the upper left with a crude drawing.
Items are added and placed on top of the pile of values, and memory addresses are represented in hexadecimal. These values decrease as we go up in the stack.



Notice that the memory locations are very much fictitious. As an added note, a memory location like 0x70 would be represented in 32 bits as:
0000 0000 0000 0000 0000 0000 0111 0000. So, if we were being very explicit in our representation, that memory address would be represented as 0x00000070.
Now that we have an initial state for our stack and for ECX which we'll use, let's carry on with the first instruction:





We are loading the effective address at ESP+4 onto ECX. Note that LEA doesn't actually access memory, it just performs address calculation.
In this context, ESP+4 is typically pointing to the program arguments.
Also, FYI, we could arguably, substitute LEA ECX, [ESP+4] with:

MOV ECX, ESP
ADD ECX, 4


But this would require two steps instead of one. Either way, it's good to know what LEA is doing behind the scenes. As we can see, the stack remains the same, but ECX is now pointing to 0x68.
Let's carry on to the second instruction:



AND: You probably remember this AND fella from boolean logic. It does exactly what it did back then. We're comparing the value stored in ESP with the value -16 and turning to 0 anything that isn't 1 in both numbers. Here's what I mean:

(ESP AND -16):
0000 0000 0000 0000 0000 0000 0110 0100
 1111 1111  1111  1111  1111  1111 1111 0000 (AND)
--------------------------------------------------------
0000 0000 0000 0000 0000 0000 0110 0000 (0x60)

What's with all those 1's, you ask? Good question. Go check out two's complement. It's really useful and it will be a great aid in the future.

This operation guarantees that the final 4 bits are zeros. Think of those bits like the remainder of a division by 16. If we have no remainder, then it means that we're working with a multiple of 16. And that's exactly why we're doing this. We're ensuring optimal performance by allowing the CPU to access memory more efficiently. Misaligned memory access can, in principle, lead to performance penalties or even crashes. 
This new position to which ESP is now pointing has 'e' stored in value, just for our sake.

Next instruction:



Now we're pushing whatever value is stored at ECX-4 (which is the previous ESP position) onto the top of the stack. So, we do just that. 'd' is sent upstairs and our ESP register follows accordingly. Why DWORD? That's a double word. And since in a 32-bit architecture words are 16 bits in size, our DWORD will be 32 bits, or 4 bytes in size. The PTR lets us know that it's a pointer to a value, so we know that we're accessing a value stored at a memory address (pointer).

Let's carry on:




That's a pretty simple one, we're pushing EBP onto the top of the stack, and so we did. We're saving the previous base pointer onto the stack and allowing the function to establish a new stack frame by setting EBP to the current value of ESP (and later restoring the previous stack frame when the function returns).

Fifth instruction:




And voila, we move EBP, setting up the new stack frame and setting EBP to the top of the stack. These two instructions allow for easy access to function parameters and local variables (relative to EBP).

Sixth instruction:




We're now pushing the value of ECX onto the stack, tracking the stack alignment and assisting in restoring the stack at the end of the function. ECX will later be used at the function epilogue to revert the stack to its original state. It's also a way to ensure that the stack is properly cleaned up when the function returns.

Final instruction of the prologue:



We're subtracting 20 from what we have at ESP, resulting in ESP = 0x40.
This creates a bubble of sorts, some room on the stack for local variables and temporary data used by the main() function.


I know... as drawings and sketches go, this one is a bit rough around the edges, but I hope it carries the message through.

Either way, you should be doing your own drawings! Start exploring this stuff until it starts making sense. A great way to learn these concepts is to keep being exposed to them, write them down, try programming them, rinse and repeat.

Have Fun!








Sunday, September 8, 2024

Wherein We Get Lost And Compare Object Dumps: C vs. Assembly

 

That's a rabbit hole, Alice. And those are books on shelves, all the way down.



Hi again!

I created a simple "Hello, World!" program in C, so that we could have a quick talk about function prologues and epilogues in Assembly, but we're in for a detour, as happens with all rabbit holes. 

And the truth is that it's just rabbit holes as we're going down (until we reach elephants, and then it's turtles all the way down, of course).

Here's the culprit:

Ok, nothing impressive, but it does its job.


After compiling this program through the usual steps, the program runs and prints "Hello, World!" to the standard output.

Next, I wanted to create an Assembly program that would print the exact same line, and although I can read some Assembly and am making progress in that front, I can't (yet) write my own Assembly programs. So I asked our LLM friend to do it for us. And so it did:



Pretty neat.

And we can turn this into a binary file with:
nasm -f elf32 print_hello_ASM.asm -o print_hello_ASM.o


And then turn it into an actual program with:
ld -m elf_i386 print_hello_ASM.o -o print_hello_ASM

And voila! We can run this program just like with our C program...

But...

 "wait, wait, wait, wait!"
You say.

"What's with the turning-the-code-into-binary-and-then-into-a-program-magic?

We don't need to compile stuff in Assembly, like we do with C?"

Well, those are great questions!

The thing is that we take compilation for granted. In fact, compilation is done in 4 steps:

- Preprocessing

- Compilation

- Assembling

- Linking

Let's ask an LLM to give us a little more information on these steps, and let it assume we want it explained in a simple manner:


Confused? Remember that you can always ask it to explain again from a different angle, in simpler terms, through analogy, etc:


We can always check more trustworthy sources, check documentation, forums, etc, like in:
https://unstop.com/blog/compilation-in-c

(I told you, it's rabbit holes most of the way down)


I'm not going to give you an in-depth explanation of these concepts (that's your job, really). But let's just say for the sake of simplicity, that when we compile our C code, we're in fact going through these four steps, and that when turning our Assembly code into a program, we just take the two last steps: Assembling and Linking (also, fyi: note that these steps can be combined or optimized in modern compilers).


To showcase the difference between these two processes and the baggage that comes along with C, let's look at an objdump of both our C and our Assembly programs.

What's an objdump? Here:



So... it's basically when we take a binary file and disassemble it back into Assembly code (+ extra info).


Then let's jump into that Assembly objdump of ours, right? Here:


And, for comparison, here's a gif with the C objdump:


Notice any difference? The C objdump file is a tad longer.
And note that I haven't included all the possible information in these dumps (checkout the man page for objdump. In particular for the -s argument).


Notice, though, that there is something we haven't seen before in our little Assembly forays. In that ASM objdump, we see these "int   0x80" lines. What are these?
Seems important enough.

These are system call interrupts, which are a way for our program to request services from the operating system's kernel. Namely, we want to be able to print our Hello World message on screen and we also want to be able to exit our program - that's what those two syscalls are doing there.

This is done behind the scenes through compilation when we're using C - so it's not all that obvious to us.


More info from our friendly LLMs:



Ah, but I just recalled that we were meant to discuss function prologues and epilogues in Assembly.

I went to https://godbolt.org/ and placed my original C code in there, and immediately got an Assembly representation of that code as well. 

And lo and behold, it's even color-coded, allowing us to see exactly what is the prologue and what is the epilogue.

Here:



But I'm leaving function prologues and epilogues for an upcoming blog post.



In the meanwhile, you can always check that yourself if you're curious. Or anything, really. See something you don't understand? Leave no stone unturned! Jump into that hole, satiate your curiosity and keep learning.



"INTs Aren't Integers and FLOATs aren't Real"

                                     I was told this is a cat-submarine. Tail = Periscope. I believe it.   Over the past few weeks, I’ve be...