1. Preface IA32 machine code and assembly code are very different from the original C code, because
some states are hidden from C programmers。 For example, a program pointer containing the next memory location to execute code (Programs counter or PC) and 8 registers. One more thing to note: The Assembly code
att formatAnd
Intel Format。 The ATT format is the default format for tools such as GCC and objdump and is used in Csapp. The Intel format is typically encountered in Intel's IA32 architecture documentation and Microsoft's Windows technical documentation. The main differences between the two are:
- The Intel format ignores the suffix in the instruction that implies the length of the operand, such as mov rather than att format movl.
- The Intel format ignores the% before the register name, such as esp instead of %espin att format.
- The Intel format describes memory locations in different ways, such as DWORD PTR [ebp+8] instead of 8 (%EBP)in att format.
- The sequence of operands of the Intel format instruction is the exact opposite of the ATT format, and the ATT format is always the last operand, such as Movl%eax, (%edx).
In addition, as a legacy of the 16-bit processor architecture, today's directives still use Word to refer to 2 bytes of 16 bits, whereas double word means 4 bytes. So the instructions typically use B, W, l to indicate that the operand is 1, 2, and 4 bytes of instruction, such as three versions of the data movement instruction Movb, MOVW, MOVL.
This chapter learns to read the underlying code by learning the machine-level underlying representation of the program. Why is reverse engineering difficult? Because the source code and the compiled code are often not one by one corresponding.
The compiler introduces new variables that do not exist in the source code, and in order to save the use of registers, the compiler often maps multiple values to a register。 For loops, some clues can be obtained by observing how the registers are initialized before the loop, the update and condition detection within the loop, and the use of the loop.
2. Registers and addressing In the first chapter of the note, we see that a large part of the program's time is moving data around. So the processor supports only 1, 2, and 4 bytes of registers, and supports multiple addressing methods. As shown in the table in the right half, we have the flexibility to load data from memory to registers, or to save values in registers to memory.
Although it may seem a bit confusing, the most basic form is actually the last one: Imm (Eb, Ei, s) =imm+r[eb]+r[ei]*s (r[x] refers to the value of register X). A total of four parameters to control the addressing, looking a bit too flexible, then let us imagine its application scenario. Regardless of IMM, the most typical application is to access an array of data items. If the array is int x[4], then the EB is the first address of the array, the equivalent of X, and EI is the subscript to access the data item, and S is the length of the data type in the array. For example we want to access x[3], then it is equivalent (x, 3, sizeof (int)) =x+3*4. Written in C is * (X+3), because the C language is automatically moved by the type length of the pointer (the compiler automatically generates the correct code), so we do not use our own calculated offsets multiplied by sizeof (int), but this is something. That, together with the IMM can have what application scenario, in fact, is very simple, is
To access an item in an array in a struct 。 As shown, an item in the array in the structure can be accessed directly by a single instruction.
3. Common Directives Here are some of the most common assembly directives and their meanings:
- mov: Data movement. IA32 imposes a restriction that the two operands of a moving instruction cannot all be memory addresses . So it takes two instructions to copy data from one memory location to another memory location.
- Leal: Load address. The effect is MOV Imm (%a,%b, s),%x will assign%x to imm+%a+s*%b instead of m[imm+%a+s*%b], so there are two useful scenarios: 1) Copy the address. For example an int *x=a assembly for MOV (%EAX),%edx, then an int x=&a assembly for Leal (%EAX),%edx. So Leal does not really save the value of a (i.e. (%EAX)) to X (that is,%edx), but simply saves the address of a (in fact,%eax) to X. 2) Simple arithmetic operation. The second very natural application is to use Leal to compress simple arithmetic operations such as Leal 7 (%edx,%edx, 4) =5x+7.
- jmp: Jumps directly to the tag, or indirectly jumps to the address specified in the register. For direct jumps, the symbolic label is usually represented in assembly language. However, the most common way to encode a assembler or linker is to have a PC-relative address. That is, an offset of 1, 2, and 4 bytes indicates the address of the next instruction that jumps to the destination address, followed by the jmp instruction , as shown in. But why is it the address of the next instruction in the JMP directive instead of the one in jmp? There are also historical reasons, because the early processor implementation is to update the PC counter as the first step, and then execute the current instruction . So the instruction in the execution, in fact, the PC has pointed to the next command, so the jump offset will be relative to the next instruction.
4. What happens when a type is converted When signed into an unsigned integer, we expect the compiler to turn negative numbers to 0, positive numbers to remain unchanged, and positive values that are longer than the maximum length are assigned to Tmax. But actually
integer conversions of the same length are simply copies and do nothing。 And when both length conversions and type conversions are required, the C language first takes the length conversion. After the length conversion, both integers become the same length, so we just need to focus on how extensions and truncation between different lengths of integers work:
- Expansion: Unsigned 0 expansion, which fills the high position with 0. Symbols are expanded with symbols to fill high positions with the highest-bit-symbol bits.
- Truncate: Simply discard the high byte. For the small end, it is the reverse, the high position of the copy Register as%al.
Since signed integers are encoded with anti-code on most machines, symbolic expansion of the inverse code does not change its value, as evidenced in the second chapter. Anti-code is so magical! 0 has a unique representation, and the symbol extension value remains the same! The key is: the high-level expansion of a 1, -2w+2w-1=-2w-1, or equal to the original value before the expansion.
5. Why does the logical operation need to be shorted In the second chapter, there are two differences between the bitwise and the logical operations, one is that only true and false in the eyes of the logical operation, and no 0 of them are considered true. The second difference is the short-circuit effect of a logical operation. Then why is the logical operation short-circuited? Because the logical operation is implemented in JMP. In assembly language, each part of the conditional expression is judged by the true and false, when a certain part of the results are directly jump. Just because
The logical operation is to decide where to run, and not like a bit operation to get a final result, so assembly language can be implemented with jump, so it produced a high-level language of the nature of short-circuit。
6. Local variables are actually in the register. In fact
local variables are stored directly in registers, and in most cases are kept in registers without landing to memory。 For example, in the 7th part of the function Swap_add (), the function runtime stack frame (memory) does not actually save any local variables. Local variables and logic for the entire function are performed in registers and Alu.
Local variables are stored in memory (on the stack) in the following cases:
- When there are not enough registers to hold all local variables. After all, there are only eight registers.
- Some local variables are arrays or structs, so they must be accessed by pointers.
- When you take an address & operation on a local variable, you must generate a memory address to it.
7. Run-time code and stack Let's look at an example of a function call and drill down to learn how the code is running at the bottom.
The caller () code is as follows:
The Swap_add () code is as follows:
The code generated by the compiler follows certain rules, so that no data overwrite occurs when performing various jumps, function calls, and so the program runs correctly.
8. The nature of the pointer Maybe I've heard it before,
The pointer is essentially a memory address。 But there was no epiphany before, and now the understanding is reinforced by studying underlying knowledge. As can be seen, the pointer value is actually a very natural operation, because most of the time we
It 's impossible to drop all the data represented in a single register., such as an array or structure. If the register can drop the entire array and structure, then of course we don't need to use pointers. So naturally, we will first load the memory address of the first address of the data (that is, the pointer!). ) to the register, and then go to the memory location where the register points.
Six-Star Classic csapp-note (3) machine-level representation of the program