I'm starting to learn C, and I just learned about pointers to functions.
This made me wonder, what does a function look like in memory? I mean, (correct me if I'm wrong) an int variable for example is just a chunk of 2/4 bytes, storing a certain whole number.
But what does a function look like in memory? How do you convert operations such as if, while, malloc, etc into numbers or binary?
What does a function look like in memory?
A function is a number of consecutive bytes. For instance in hex a function could be:
A2 32 67 98 34 33 12 23 22 55
These (hex) numbers represents instruction that the hardware, i.e. the CPU, can execute. The CPU decodes the values and finds out what it is going to do. In other words - which instructions to execute.
Exactly how these numbers translate into instructions differs from CPU family to CPU family. Sometimes a single byte is a whole instruction. Sometimes it takes a number of bytes to make an instruction. To figure out how to translate the bytes into instructions, you need to consult the instruction set of the CPU being used.
In some cases instructions are pretty simple. For instance "ADD register 1 and register 2", "JUMP-IF-ZERO register 3, address", "DECREMENT register 5", "LOAD register 1 from a memory address", "STORE register 1 to a memory address" and so on. But you can also find CPUs with pretty complex instructions.
? How do you convert operations such as if, while,
By using a number of the simple instructions from the instruction set.
Example: With the simple instructions I made up above and assuming that x is integer located at address 0x100 in memory, the C code:
x = x - 1;
if (x != 0)
{
x = x - 1;
}
could be something like
LOAD reg1, 0x100 // Read x from memory address 0x100 into register 1
DECREMENT reg1
JUMP-IF-ZERO reg1, LABEL1
DECREMENT reg1
LABEL1:
STORE reg1, 0x100 // Write x back to memory
Notice that a specific C code often can be made in several ways on a specific CPU. Different compilers may generate a different sequence of instruction for the same C code.
The answer to your question is machine code.
Run your compiled code in a source-level debugger and switch on the "disassembly view" to see exactly how the source is translated to machine code (typically presented as a "disassembly" which is a human readable representation of the machine code - "assembly code"). Or use Compiler Explorer which presents the assembly code generated for many different compilers for various architectures and machine instruction sets. For example Compiler Explorer shows that the following C source code:
int fn(void)
{
return 11 ;
}
int main()
{
int (*fn_ptr)(void) = fn ;
int val = fn_ptr() ;
if( val > 10 )
{
val = 10 ;
}
return 0 ;
}
using MSVC v19 x64 compilation translates to:
fn PROC
mov eax, 11
ret 0
fn ENDP
val$ = 32
fn_ptr$ = 40
main PROC
$LN4:
sub rsp, 56 ; 00000038H
lea rax, OFFSET FLAT:fn
mov QWORD PTR fn_ptr$[rsp], rax
call QWORD PTR fn_ptr$[rsp]
mov DWORD PTR val$[rsp], eax
cmp DWORD PTR val$[rsp], 10
jle SHORT $LN2@main
mov DWORD PTR val$[rsp], 10
$LN2@main:
xor eax, eax
add rsp, 56 ; 00000038H
ret 0
main ENDP
Each assembly line represents a single machine language instruction (with the exception of some "pseudo-ops" such as ENDP and PROC which are directives to the assembler tool), and has a corresponding binary representation in memory. For example the machine code corresponding to mov eax,11 in the function fn() is:
b8 0b 00 00 00 // Load (move) the literal value 0x0000000B to register EAX
where b8 is the opcode for MOV in hexadecimal (base 16) notation, and
0b 00 00 00 is the operand 11 as a 32-bit little-endian (least significant byte first) integer. Note that hexadecimal is commonly used to present binary data because a single hex digit corresponds to exactly 4 binary digits (bits), and reading long strings of 1's and 0's is impractical. For example b8 0b 00 00 00 in binary, hexadecimal and decimal is:
1011 1000 0000 1011 0000 0000 0000 0000 0000 0000 // Binary
B 8 0 B 0 0 0 0 0 0 // Hex (nibbles)
184 11 0 0 0 // Decimal
See https://www.cs.uaf.edu/2016/fall/cs301/lecture/09_28_machinecode.html for additional examples of x86 machine code and corresponding assembly.
As I mentioned at the start, you can use a source-level debugger to directly inspect the relationship between source code and the machine code translation. For example the following is an example in VS Code showing the source (C++ code in this case, but the principle is the same), and the corresponding assembly code and its corresponding machine code in hexadecimal:
[source: https://user-images.githubusercontent.com/80216624/115053737-6a850280-9f1a-11eb-8505-a7c5570af71a.gif]
See how the cursor in the source matches the instruction at the cursor in the disassembly. Not that it is not always that straightforward in optimised code, where there is often a less direct correspondence between source and machine code (and also why source-level debugging is normally performed on un-optimised code).
Note also that each instruction has an address. The address 0x00000001400BDF1B in this case is the address of the function func(), so would be the value of a pointer-to-func() (i.e. the value of func).
Another example from Visual Studio 2022 using the same code as the Compiler Explorer example above:
Note that the code generation differs from that in Compiler Explorer - different compiler version, different compiler settings. Also in this example the generating source is interspersed, with line number references so you can see exactly how each line is translated - noting that this generally only makes sense for unoptimized code use in debugging.
Further reading on how computers work at the lowest possible level: Code The Hidden Language of Computer Hardware and Software
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With