深入理解计算机系统——第三章 Machine-Level Representation of Programs

图解系统
程序的机器级表示(一)
程序的机器级表示(二)
程序的机器级表示(三)
程序的机器级表示(四)
深入理解计算机系统学习笔记
程序的机器级表示
深入理解计算机系统(CSAPP)全书学习笔记(详细)
深入理解计算机系统 课程视频
汇编语言各种指令的解释与用法
cmov条件传送指令
第18章-x86指令集之常用指令

3.2 Program Encoding

C 程序从源文件生成可执行文件的过程:

  1. The C Preprocessor expands the source code to include any files specified with #include commands and to expand any macros, specified with #define declarations.

  2. The compiler generates assembly code versions of the source files (*.s).

  3. The assembler converts the assembly code into binary object-code files (*.o). Object code is one form of machine code—it contains binary representations of all of the instructions, but the addresses of global values are not yet filled in.

  4. The linker merges these object-code files along with code implementing library functions (e.g., printf) and generates the final executable code file (as specified by the command-line directive -o p). Executable code is the second form of machine code we will consider—it is the exact form of code that is executed by the processor.

3.2.1 Machine-Level Code

  1. The format and behavior of a machine-level program is defined by the instruction set architecture, or ISA, defining the processor state, the format of the instructions, and the effect each of these instructions will have on the state.

  2. The memory addresses used by a machine-level program are virtual addresses, providing a memory model that appears to be a very large byte array.

Whereas C provides a model in which objects of different data types can be declared and allocated in memory, machine code views the memory as simply a large byte-addressable array.

指令集介绍:

CPU 指令集
What Is an Instruction Set Architecture?

虚拟地址介绍:

Windows内存体系(1) – 虚拟地址空间
virtual address

3.3 Data Formats

Due to its origins as a 16-bit architecture that expanded into a 32-bit one, Intel uses the term “word” to refer to a 16-bit data type. Based on this, they refer to 32- bit quantities as “double words,” and 64-bit quantities as “quad words.”
Sizes of C data types in x86-64

3.4 Accessing Information

An x86-64 central processing unit (CPU) contains a set of 16 general-purpose registers storing 64-bit values. These registers are used to store integer data as well as pointers.

Integer registers

整数寄存器,用来存放整型指针,有6个寄存器用来存放6个参数,如果参数超过6个,则放在上。

3.4.1 Operand Specifiers

x86-64 支持操作数形式如下:

Operand forms

源操作数有三种:立即数(常数),寄存器,内存
目标操作数:寄存器或者内存; 结果存放的位置

立即数(immediate):常数值。
内存引用(memory reference):根据有效地址获取内存的位置。

  1. 对于 ATT 形式的汇编代码,用 $ 跟着一个整型(C 标准格式)表示立即数,如,:$-577$0x1F,因此指令 $-577 表示常数 -577。 不同的指令允许的立即数的范围不同。

  2. r a r_{a} ra 表示一个寄存器, R [ r a ] R[r_{a}] R[ra] 表示寄存器 r a r_{a} ra 的值,将一组寄存器看作数组 R R R,而寄存器 r a r_{a} ra 为数组的索引。

  3. 内存被看作一个大的字节数组,因此用 M b [ A d d r ] M_b[Addr] Mb[Addr] 表示对内存中起始地址为 Addrb 字节的数值的引用。这里去掉下标 b

  4. M [ I m m ] M[Imm] M[Imm] 是绝对寻址,直接给内存的地址(立即数),指令的格式为一个立即数 Imm

  5. M [ R [ r a ] ] M[R[r_{a}]] M[R[ra]] 是间接对寻址,获取寄存器 r a r_{a} ra 的值作为内存的其实地址,指令的格式为 ( r a ) (r_{a}) (ra)

  6. 对于指令格式 I m m ( r b , r i , s ) Imm(r_{b}, r_{i}, s) Imm(rb,ri,s) r b r_{b} rb 为基址寄存器, r i r_{i} ri 为变址寄存器, s s s 为比例因子,其值必须为 1,2,4或8(对应 byte,word,double word, quad word 大小)。基址和变址寄存器必须为 64 位寄存器(图 3.2 中最左边一列)。最后有效地址的计算公式为: I m m + R [ r b ] + R [ r i ] ⋅ s Imm + R[r_{b}] + R[r_{i}] \cdot s Imm+R[rb]+R[ri]s


示例:
practice3.1

3.4.2 Data Movement Instructions

MOV 指令:These instructions copy data from a source location to a destination location, without any transformation.

四种形式:movb, movw, movl, and movq,效果相同,只是数据大小不同,分别为 1,2,4,8 字节:


Simple data movement instructions


示例:

指令解释
movl $0x4050,%eaxImmediate–Register, 4 bytes
movw %bp,%spRegister–Register, 2 bytes
movb (%rdi,%rcx),%alMemory–Register, 1 byte
movb $-17,(%esp)Immediate–Memory, 1 byte
movq %rax,-12(%rbp)Register–Memory, 8 bytes
  1. MOV 指令源操作数和目标操作数不能同时为内存,因为将数据从一个内存地址复制到另一个内存地址需要两条指令完成,需要先将源地址的数据放到一个寄存器中,再从该寄存器的数值写到目标内存地址。
  2. 指令和所用寄存器大小需匹配,如用 movl 时,寄存器用 32 位寄存器。
  3. 通常,mov 指令只会根据目标操作数来更新寄存器中的特定位,如用 movabsq 将立即数复制到64位寄存器中,则目标寄存器的 64 位都用到,更新数据,源数据没有 64 位,则高位用 0 填充;而对于 movw指令,目标寄存器位 16 位,因此数据只更新低 16 位,高位不更新;但 movl 是一个特例,更新低 32 位后,会将高 32 位填充为 0。见下面例子,第一次用 movabsq 指令后更新 %rax 的全部 64 位数,之后用 movb 指令,目标寄存器是 %al ,即 %rax 寄存器的低 8 位,源操作数 -1 的补码为 FF,因此低 8 位更新为 FF,其他高位不变:
    mov
  4. 当用 movq 指令且源操作数立即数时,立即数只能是 32 位的补码,而不能是 64 位,高位将用符号位扩展从而得到 64 位(第二章介绍过符号位扩展补码的高位,其结果不变)。
  5. movabsq 指令时,源操作数可以是任意的 64 位立即数,目标操作数只能是寄存器。

当目标操作数尺寸大于源操作数,可以在高位进行零扩展或者符号位扩展

零扩展 movz 系列指令:剩下的高位填充 0。

Zero-extending data movement instructions

注意:这里没有 movzlq,因为当使用 movl 指令时,且目标操作数为寄存器,那么寄存器的高4个字节会填充 0

要求:源操作数为寄存器或者内存地址,目标操作数位寄存器。


符号位扩展 movs 系列指令:剩下的高位用符号位填充:

Sign-extending data movement instructions

示例:
fig 3.7

第一个参数 xp 和第二个参数 y 分别存在寄存 %rdi%rsi 中。
指令 2:根据 xp 地址从内存中取数值,存到寄存器 %rax 中。
指令3:将 %rsi 的值,即 y 的值存到地址为 xp 的内存中。
最后将寄存器 %rax 的值,即 x 作为返回值返回。

上述的局部变量保存在寄存器而非内存中,寄存器比内存的访问速度更快。


示例1:
practice 3.2

根据图 3.2 可知所用寄存器大小,题解中说对于 x86-64,内存引用总是 quad word。


示例2:
practice 3.3


示例3:
practice 3.5

  1. 因为参数类型为指针,用 8 字节寄存器。
  2. 根据图 3.2 可知,第一个到第三个参数分别为 %rdi,%dsi 和 %rdx,三个寄存器的值分别为 xp,yp,和 zp,为地址。
  3. 第一个指令源操作数为内存引用,内存地址为 xp,从内存地址 xp 处取数值,存放到寄存器 %r8 中,用 x 代表 %r8 的值,则该指令对应 x = *xp
  4. 第二个和第三个指令同第一个类似,分别用 y 和 z 代表寄存器 %rcx 和 %rax 的值,则这两条指令分别为 y = *ypz = *zp
  5. 第四条指令源操作数 %r8 的值,即 x,目标操作数为内存地址 yp,则将 x 存放到内存地址为 yp 处,即 *yp = x
  6. 第五条指令和第六条指令同第四条指令类型,分别为 *zp = y*xp = z

因此对应的 C 代码如下:
solution 3.5

3.4.4 Pushing and Popping Stack Data

  • (stack)是一种数据结构,遵循先进后出的规则,可以增加(push)或删除(pop)数据。

  • 可以由数组来实现,数组的末尾为栈顶,添加或删除元素在栈顶操作。

  • 程序用栈来管理过程调用返回的状态

  • 对于 ×86-64,栈存在内存的某个区域,且向下增长,即栈底在高位地址处,当 push 指令添加数据时,栈顶向低位扩展栈指针 %rsp 指向栈顶位置。

  • 当使用 pop 弹出栈顶的数据时,实际数据仍在该地址,只是栈顶指针指向的位置变了(向高地址移动8)。

PushPop 指令:
Push and pop instructions

栈操作介绍:
Illustration of stack operation

3.5 Arithmetic and Logical Operations

每一种类型的指令都会对应操作 4 种不同大小数据的指令。
例如,加法指令分为 addb, addw, addl, 和 addq 分别操作大小为 1字节,2字节,4字节,和 8 字节大小的数据。

The operations are divided into four groups: load effective address, unary, binary, and shifts.

3.5.1 Load Effective Address

load effective address 指令是 movq 指令的一个变体,从内存读数据到寄存器,但不是内存引用,而是复制内存地址到寄存器

Integer arithmetic operations

例如:%rax 的值为 x,则指令 leaq 7(%rdx,%rdx,4), %rax 表示将寄存器 %rax 的值设为 7 + 5x,这个过程只复制内存地址而不访问内存; 而如果是 movq 指令,则是将内存地址为 7 + 5x 的值设置为寄存器 %rax 的值。


示例:
practice 3.7

  1. 第一个参数 x 存在寄存器 %rdi 中,第二个参数 y 存在寄存器 %rsi 中,第三个参数 z 存在寄存器 %rdx 中。
  2. 第一个指令源操作数为地址 10 * y,leaq 指令只复制地址而不是同 mov 指令一样访问内存,因此该指令将 %rbx 的值设置为 10 * y。
  3. 第二个指令将 %rbx 设置为 10 * y + z。
  4. 第三个指令将 %rbx 设置为 10 * y + z + x * y。
  5. 最后返回为 %rbx 的值 ??因为 %rbx 是被调用者保存数据的寄存器 (callee saved)?

3.5.2 Unary and Binary Operations

图 3.10 中第一组为一元运算, INC 指令相当于 C 语言的自增运算,如果是 incq (%rsp) 则会使栈顶的地址加 8 字节。

图 3.10 中第三组加减等操作为二元运算,例如 subq %rax,%rdx 指令会将寄存器 %rdx 的值设置为 原始 %rdx 的值减去 %rax 的值。
第三组的二元运算的源操作数(第一个操作数)可以是立即数,寄存器 和 内存地址,而目标操作数(第二个操作数)只能是寄存器或内存地址,同 mov 指令相同,两个操作数不能同时为内存地址
当第二个操作数为内存地址时,处理器从内存地址读数据,执行运算操作后,将结果写回到该内存地址。


示例:
practice 3.8

答案如下:

solution 3.8

  1. 第一条指令,源操作数为寄存器 %rcx 的值 0x1,目标操作数为内存地址 0x100 (寄存器 %rax 的值),将 0x 100 的值设置为 0xFF ( 初始内存中的值) + 0x01 (%rcx 的值) = 0x100。
  2. 第二条指令,目标操作数为内存地址 8 + 0x100 = 0x108,执行减法指令:0xAB(内存地址为 0x108 的值) - 0x3(%rdx 的值)= 0xA8,最后将计算结果写回到内存地址 0x108 处。
  3. 第三条指令,目标操作数为内存地址 0x100 + 0x3 * 8 = 0x118,执行乘法指令:0x10 (立即数 16)* 0x11(内存地址为 0x118 的值) = 0x110,最后将计算结果 0x110 写回内存地址 0x118 处。
  4. 第四条指令,目标操作数为内存地址 0x100 + 0x10(立即数 16) = 0x110,从该地址取值 0x13,将加减1,得到 0x14,然后写回内存地址 0x110 处。
  5. 第五条指令,两个操作数均为寄存器,目标操作数为寄存器 %rax,%rax 的值 0x100 - %rdx 的值 0x3 = 0xFD,将值设置为寄存器 %rax 的值。

3.5.3 Shift Operations

图 3.10 中的最后一组为移位运算,第一个数表示需要移位的数目,第二个操作数为需要移位的数
第一个操作数可以是立即数或者单字节的寄存器 %cl
如果第二个操作数是寄存器 %cl,对于 x86-64,如果一个数有 w 位,m 满足 2 m 2^m 2m = w w w,则移位的数目为 %cl 的最低 m 位数据的值。
例如 %cl 的值为 0xFF,那么 salb 指令移位的数目为 7,因为 salb 操作的数 8 位,m 为 3,%cl 的低三位为 111,即 7。

左移指令 SAL 和 SHL 相同,都是向右边填充 0。
右移指令 SAR 和 SHR 分别表示算术右移逻辑右移算术右移向高位填充符号位逻辑右移向高位填充 0


示例:
practice 3.9

3.5.4 Discussion

图 3.10 中的指令能用于无符号数或者补码计算,有符号和无符号只有右移运算有区别,第二章有介绍相关知识。

示例 1:
fig 3.11

上面计算第二个乘法语句 z * 48 使用了两条指令,先计算 3 * z,然后左移 4 位,因为左移 1 位相当于乘以 2,因此结果位 3 * z * 16 得到 z * 48。


示例2:
practice 3.10


示例3:
practice 3.11

  1. 该异或的指令可以将寄存器的值设置为 0,相同的位异或后为0,因此将 %rcx 的值与自己异或后为0,然后将结果 0 设置为 %rcx 的值。
  2. 可以直接设置该值为 0:movq $0, %rcx
  3. 没明白,答案说任何指令更新低位的 4 字节会造成高位 4 字节为 0 (??没明白),因此异或时只用处理低 4 字节,用 xorl %ecx,%ecx;或者用 movl $0, %ecx,因为 movl 会将高位设置为 0 (前面有讲)。
    solution 3.11

3.5.5 Special Arithmetic Operations

特殊的算数操作:
Special Arithmetic Operations

3.6 Control

3.6.1 Condition Codes

In addition to the integer registers, the CPU maintains a set of single-bit condition code registers describing attributes of the most recent arithmetic or logical operation.

These registers can then be tested to perform conditional branches.

3.5.1节中的 fig 3.10 除了 leaq 指令外都会改变下面的某些条件代码寄存器的值。

condition codesCF: 进位标志

比较和测试指令

汇编语言CMP(比较)指令:比较整数
汇编语言TEST指令:对两个操作数进行逻辑(按位)与操作
汇编语言各种指令的解释与用法

Comparison and test instructions

比较指令:如果相等设置 ZF标志位为 1。
测试指令:和 AND 指令使用相同,但不会修改内容(前面可知 AND 指令会将计算结果更新到目标操作数中)。

3.6.2 Accessing the Condition Codes

SET 指令:
The set instructions

SET 的目标操作数:图 3.2 中最右侧低位 1 字节的寄存器,或者单字节的内存地址,其结果为 0 或者 1。
如果想得到 32 位 或者 64 位 的结果,需3.9要将高位清零。

3.6.3 Jump Instructions

jump 指令:
The jump instructions

jmp 指令有两种跳转方式:

  • direct jump
    The jump target is encoded as part of the instruction:
    在这里插入图片描述

  • indirect jump
    The jump target is read from a register or a memory location.
    Indirect jumps are written using * followed by an operand specifier using one of the memory operand formats described in Figure 3.3.
    1

3.6.4 Jump Instruction Encodings

There are several different encodings for jumps.

  • PC relative
    They encode the difference between the address of the target instruction and the address of the instruction immediately following the jump. These offsets can be encoded using 1, 2, or 4 bytes.

  • absolute address
    It uses 4 bytes to directly specify the target.

3.6.6 Implementing Conditional Branches with Conditional Moves

cmov条件传送指令
第18章-x86指令集之常用指令

When the machine encounters a conditional jump (referred to as a “branch”), it cannot determine which way the branch will go until it has evaluated the branch condition.
跳转指令效率低:机器遇到条件跳转时,无法知道跳转到哪条分支,因此会做分支预测来猜测可能会跳转到哪条分支,如果预测失败,则需要舍弃之前的工作然后执行正确的分支。

Unlike conditional jumps, the processor can execute conditional move instructions without having to predict the outcome of the test.
条件传送指令无需预测,效率更高。

条件传送指令:

The conditional move instructions

条件转移示例:
如果要做如下判断:

v = test-expr ? then-expr : else-expr;

写成如下形式:

v = then-expr;
ve = else-expr;
t = test-expr;
if (!t) v = ve;

注意:
1、不是所有情况都能用条件转移
如:

long cread(long *xp) 
{
	return (xp ? *xp : 0);
}

如果用条件转移:

long cread(long *xp)
Invalid implementation of function cread
xp in register %rdi
1 cread:
2 movq (%rdi), %rax       v = *xp
3 testq %rdi, %rdi        Test x
4 movl $0, %edx Set       ve = 0
5 cmove %rdx, %rax        If x==0, v = ve
6 ret Return v

那么在第 2 行 movq 指令中,当 xp 为空指针时,也会获取其值,将会出错。

2、不是所有用条件转移的情况效率都会更高
如果条件判断语句中有大量的计算过程,那么会浪费大量的工作在错误的分支计算中。

因此,只有当条件表达式的计算较简单时采用条件转移

3.6.7 Loops

do-whilewhilefor 通过条件判断和跳转实现循环控制。

3.6.8 Switch Statements

A switch statement provides a multiway branching capability based on the value of an integer index.

Not only do they make the C code more readable, but they also allow an efficient implementation using a data structure called a jump table.

A jump table is an array where entry i is the address of a code segment implementing the action the program should take when the switch index equals i.

The code performs an array reference into the jump table using the switch index to determine the target for a jump instruction.

The advantage of using a jump table over a long sequence of if-else statements is that the time taken to perform the switch is independent of the number of switch cases.

Jump tables are used when there are a number of cases (e.g., four or more) and they span a small range of values.

switch 比用大量的 if-else 效率高。

3.7 Procedures

过程是一个抽象,隐藏实现的细节,提供接口使用。对于不同的编程语言,其叫法不同,但本质特征一样。

示例:

long mult2(long, long);
void multstore(long x, long y, long *dest) 
{
	long t = mult2(x, y);
	*dest = t;
}

汇编代码:

multstore:
pushq %rbx
movq %rdx, %rbx

call mult2
movq %rax, (%rbx)
popq %rbx
ret

P 调用过程 Q,将执行以下操作:
1、传递控制 Passing control
调用 call 指令,首先减小栈指针地址(8位)更新栈顶位置,然后将这条调用指令之后的指令地址写入栈顶(调用返回后执行的指令地址),程序计数器将被设置为被调用函数的首地址(该地址存在 call 指令中),该指令结合了 jumppush 的功能。

当被调用的过程 Q 执行完成,会执行 ret 指令(或 retq),该指令就是逆转 call 指令的效果。它会假设栈顶有一个想要跳转的地址,然后将栈顶的地址弹出(pop指令,弹出后栈顶的指针会增加,而该地址的内容不会消失,只是不属于栈的一部分),然后将程序计数器设置为弹出的地址,因此程序会回到原来的地方继续执行。

2、传递数据 Passing data
存放参数的寄存器有6个:%rdi, %rsi, %rdx, %rcx, %r8, %r9,存放返回值的寄存器位 %rax
上述这些寄存器只能存放整型和指针,如果参数超过6个,则参数会被放入栈中。(参数放在寄存器中比栈中访问速度更快)

3、管理和释放内存 Allocating and deallocation memory
被调用的函数 Q 必须为局部变量分配空间,并且在返回前释放存储空间。

栈结构:
General stack frame structure

3.7.1 The Run-Time Stack

Using our example of procedure P calling procedure Q, we can see that while Q is executing, P, along with any of the procedures in the chain of calls up to P, is temporarily suspended.
Q 正在被执行时,P 以及与其相关的调用 P 的过程都处于挂起状态。

执行特定函数时,只需要引用该函数内部的数据或者已传递给它的值,而其他函数处于冻结状态,这种为单线程运行模式

栈帧(stack frame):栈上用于特定 call 的每个内存块成为栈帧(It is a frame for a particular instance of a procedure, a particular call to a procedure)。

我们需要在中为每个被调用且未返回的过程保留一个栈帧

通常一个栈帧两个指针分隔,一个是栈指针(指向栈顶),另一个是基指针(base pointer),由寄存器 %rbp 保存,基指针是可选的

递归的弊端:会不断增加栈帧,需要很多空间,而大多数系统会限制栈的深度

P 调用 Q 时,会将 Q 返回后地址(返回后要执行的位置)放在栈上,该返回地址也被认为是 P 栈帧的一部分。

大多数过程的栈帧固定的尺寸,在过程开始执行时就分配好空间,但有些情况需要可变大小的栈帧,如过程中传递的参数数目大于6个时,会在 Q 被调用前将多余的参数存放在 P 的栈帧上。

并非所以的函数都会用到栈帧,当一个函数的全部局部变量都存在寄存器中,且函数内部不需要调用其他函数,不需要栈帧

3.7.4 Local Storage on the Stack

At times, however, local data must be stored in memory. Common cases of this include these:

  • There are not enough registers to hold all of the local data.
  • The address operator & is applied to a local variable, and hence we must be able to generate an address for it.
  • Some of the local variables are arrays or structures and hence must be accessed by array or structure references.

3.7.5 Local Storage in Registers

Although only one procedure can be active at a given time, we must make sure that when one procedure (the caller) calls another (the callee), the callee does not overwrite some register value that the caller planned to use later.

当一个过程调用另一个过程时,必须保证被调用者不会覆盖调用者以后需要使用的寄存器

By convention, registers %rbx, %rbp, and %r12–%r15 are classified as callee saved registers.

P 调用 Q时,Q 必须保证那些在调用完后 P 仍需使用的寄存器的值在调用前后保持不变(如寄存器 %rdi (第一个参数的值)),可以有两种方式:
1、保证 Q 不会改变这些寄存器的值。
2、在 Q 被调用前先将这些寄存器的值压入中,然后在调用结束弹出这些值。这种方式在压入栈中是在栈帧中创建的区域被标记为 被保存的寄存器(Saved register)

临时存放寄存器的值有两种:

  • Caller Saved
    调用者在调用前临时值存放在它的栈帧中。
  • Callee Saved
    被调用者在执行前将临时值存在它的栈帧中,然后在返回到调佣处前恢复这些值。

寄存器 %r10%r11Caller Saved 寄存器,用于存放任何可以被函数修改的临时值

寄存器 %rbx, %rbp%r12–%r15Callee Saved 寄存器,当一个函数要改变这些寄存器的值时,必须先压入栈中保存再在返回时从栈中弹出恢复数据

示例:

Code demonstrating use of callee-saved registers

上述代码使用 callee-saved 寄存器 %rbp来存放 x 的值,用寄存器 %rbx 来存放 Q(y) 计算的结果。在函数的最开始,先将这两个寄存器的内容压入栈中,最后在函数的结果再弹出栈的内容到寄存器中。
注意压入栈和弹出栈时的顺序相反(先进后出)。

3.8 Array Allocation and Access

机器代码里没有数组的概念,数组即为连续的字节集合。

3.8.1 Basic Principles

For data type T and integer constant N, consider a declaration of the form T A[N];

标注数组起始地址为 x A x_{A} xA,该数组将分配一块连续的内存空间,大小为 L ⋅ N L \cdot N LN,其中 L 为数组元素类型 T 的大小(bytes),N 为数组元素的个数。

标志符 A 将被用作一个指向数组起始地址的指针,数组元素的索引在 0N-1 区间,第 i 个元素的索引为 x A + L ⋅ i x_{A} + L \cdot i xA+Li

The memory referencing instructions of x86-64 are designed to simplify array access.

假设 E 是一个元素为 int 型的数组,E 的地址存在寄存器 %rdx 中,而索引 i 则存在寄存器 %rcx 中,如果想获取 E[i],如下指令:

movl (%rdx,%rcx,4),%eax

将实现 x E + 4 i x_{E} + 4i xE+4i,从该地址的内存处读取数据,然后存在寄存器 %eax 中(%eax 是 32 位存返回值的寄存器,%rax 是64位 存放返回值寄存器)。

比例因子(scaling factors)的值 可以是 1,2,4 和 8,分别对应前面说的四种数据大小,上面例子为 4,表示数组元素大小为 4 字节。

3.8.2 Pointer Arithmetic

C allows arithmetic on pointers, where the computed value is scaled according to the size of the data type referenced by the pointer.
That is, if p p p is a pointer to data of type T T T , and the value of p p p is x p x_{p} xp, then the expression p + i p+i p+i has value x p + L ⋅ i x_{p} + L \cdot i xp+Li, where L L L is the size of data type T T T .

示例:
Examples

上述例子返回值的存放:the result being stored in either register %eax (for data) or register %rax (for pointers).

述返回结果为 int 类型的数组元素值时,用 movl 和寄存器 %eax ,而返回值为 int * 指针时,为 leaqleaq 指令不是引用内存而是复制地址)和寄存器 %rax

最后一个例子展示计算有相同结构的两个指针相减,返回结果类型为 long,数值等于两个地址的差值除以元素类型的大小,即为两个地址相差的元素个数。

3.8.3 Nested Arrays

多维数组:对于数组 T D[R][C],数组元素 D[i][j] 在内存中的地址为:
& D [ i ] [ j ] D[i][j] D[i][j] = x D + L ( C ⋅ i + j ) x_{D} + L(C \cdot i + j) xD+L(Ci+j)

其中 L 是数据类型 T 的大小(bytes)。

数组的结构如下图:

Nested Arrays

3.8.4 Fixed-Size Arrays

示例:下面例子计算两个数组的相乘,数组 A 的第 i 行每个元素分别乘以数组 B 的第 k 列的每个元素,下面代码展示了优化前和优化后的两种实现方式。

#define N 16  //通过 #define 来声明常量,便于修改尺寸
//typedef 声明 fix_matrix 为一个二维数组,有 N 行,N列,元素类型为 int
typedef int fix_matrix[N][N];  

fig 3.37

优化后的汇编代码如下:

在这里插入图片描述
在这里插入图片描述

salq 为左移指令,对于 & A [ i ] [ 0 ] A[i][0] A[i][0],根据公式 & D [ i ] [ j ] D[i][j] D[i][j] = x D + L ( C ⋅ i + j ) x_{D} + L(C \cdot i + j) xD+L(Ci+j),可得到 x A + 64 i x_{A} + 64i xA+64i,而 i 左移 6 位即为 i ∗ 2 6 i * 2^6 i26

13 行处比较的结果会保存在 ZF 标志中,相等则为1,然后根据 jne 指令,即判断 ZF 标志,不为 0,即不相等就跳转到标签 L7 处。

3.8.5 Variable-Size Arrays

Historically, C only supported multidimensional arrays where the sizes (with the possible exception of the first dimension) could be determined at compile time.

Programmers requiring variable-size arrays had to allocate storage for these arrays using functions such as malloc or calloc, and they had to explicitly encode the mapping of multidimensional arrays into single-dimension ones via row-major indexing.

可变大小的多维数组 int A[expr1][expr2] ,定义如下函数:

int var_ele(long n, int A[n][n], long i, long j) 
{
	return A[i][j];
}

注意 n 要在 A[n][n] 前声明,汇编代码如下:

referencing function

这里计算 n ⋅ i n \cdot i ni 用到 imulq 乘法指令,而非用左移指令。

示例:
fig 3.38

优化后的汇编代码:

assembly code

3.9 Heterogeneous Data Structures

3.9.1 Structures

The different components of a structure are referenced by names.

The implementation of structures is similar to that of arrays in that all of the components of a structure are stored in a contiguous region of memory and a pointer to a structure is the address of its first byte.

例如对以下结构体:

struct rec 
{
	int i;
	int j;
	int a[2];
	int *p;
};

其结构如下:
structure
可见数组是嵌入在结构体中。
例如变量 r 的类型为 rec *,获取结构体中成员的汇编代码:

Registers: r in %rdi
1 movl (%rdi), %eax 		Get r->i
2 movl %eax, 4(%rdi) 		Store in r->j

上述代码第一条为获取 i 的数值,然后存到返回值寄存器 %eax 中;
第二条为将寄存器 %eax 的内容写入到变量 r 地址加 4 后的地址所在的内存处,即 j 的地址处,因此将 j 的数值设置为 i 的数值。

To generate a pointer to an object within a structure, we can simply add the field’s offset to the structure address.
For example, we can generate the pointer &(r->a[1]) by adding offset 8 + 4 ⋅ 1 = 12 8 + 4 \cdot 1= 12 8+41=12.
For pointer r in register %rdi and long integer variable i in register %rsi, we can generate the pointer value &(r->a[i]) with the single instruction:

Registers: r in %rdi, i %rsi
1 leaq 8(%rdi,%rsi,4), %rax 		Set %rax to &r->a[i]

The selection of the different fields of a structure is handled completely at compile time.

3.9.2 Unions

共用体:allowing a single object to be referenced according to multiple types.

与结构体的区别:Rather than having the different fields reference different blocks of memory, they all reference the same block.

共用体的大小为共用体中最大类型的变量的大小

共用体的使用场景
1、One application is when we know in advance that the use of two different fields in a data structure will be mutually exclusive. Then, declaring these two fields as part of a union rather than a structure will reduce the total space allocated.
提前知道需要使用几种不同的类型,且不同类型是互斥的使用。

2、Unions can also be used to access the bit patterns of different data types.
例如:需要做如下类型转换

unsigned long u = (unsigned long) d;

通过以下方式实现:

unsigned long double2bits(double d) 
{
	union {
		double d;
		unsigned long u;
	} temp;
	temp.d = d;
	return temp.u;
};

The result will be that u will have the same bit representation as d, including fields for the sign bit, the exponent, and the significand.

注意事项
When using unions to combine data types of different sizes, byte-ordering issues can become important.

3.9.3 Data Alignment

结构体字节对齐,C语言结构体字节对齐详解

数据对齐:基于硬件的需求
Alignment restrictions simplify the design of the hardware forming the interface between the processor and the memory system.

使用数据对齐能提升内存系统的性能。

对齐的规则:
Their alignment rule is based on the principle that any primitive object of K bytes must have an address that is a multiple of K. We can see that this rule leads to the following alignments:

alignment rule

具体 K 的大小是多少,由结构体中尺寸最大的类型对应的 K 决定。

如:

struct S1 
{
	int i;
	char c;
	int j;
};

对齐后如下,第二个数据 c 会填充 3 个字节来满足对齐:
alignment

有时编译器可能需要在结构体末尾填充,如:

struct S2 
{
	int i;
	int j;
	char c;
};

对齐后:
alignment

3.10 Combining Control and Data in Machine-Level Programs

3.10.3 Out-of-Bounds Memory References and Buffer Overflow

GDB 调试的命令:
Example gdb commands

3.10.4 Thwarting Buffer Overflow Attacks (?)

未看
缓冲区溢出解决方案:

  1. Stack Randomization
  2. Stack Corruption Detection
  3. Limiting Executable Code Regions

3.10.5 Supporting Variable-Size Stack Frames

Some functions, however, require a variable amount of local storage.

This can occur, for example, when the function calls alloca, a standard library function that can allocate an arbitrary number of bytes of storage on the stack.

It can also occur when the code declares a local array of variable size.

示例:

long vframe(long n, long idx, long *q) 
{
	long i;
	long *p[n];
	p[0] = &i;
	for (i = 1; i < n; i++)
		p[i] = q;
		
	return *p[idx];
}

上述代码包含可变尺寸的数组,数组 p 为包含 n 个指向 long 的指针,不同调用函数可能传递的 n 的值不同,而该数组需要在栈上分配 8n 字节,因此编译器无法知道为该函数的栈帧分配多少空间。

此外,该函数用到了对局部变量 i 的地址的引用,因此该变量必须存在栈上。

To manage a variable-size stack frame, x86-64 code uses register %rbp to serve as a frame pointer (sometimes referred to as a base pointer, and hence the letters bp in %rbp).

栈帧的结构如下:
Stack frame structure

We see that the code must save the previous version of %rbp on the stack, since it is a callee-saved register.
It then keeps %rbp pointing to this position throughout the execution of the function, and it references fixed-length local variables, such as i, at offsets relative to %rbp.

汇编代码如下:
Function requiring the use of a frame pointer

  1. 第 2 行,保存当前 %rbp 寄存器的值到栈中。
  2. 第 3 行,将 %rbp 寄存器的值设置为栈指针位置 %rsp
  3. 第 4 行,将栈指针位置向下扩展16 字节,前 8 个字节存放局部变量 i,后 8 个字节未使用(unused)。
  4. 第 5 行,将寄存器 %rax 的值设置为 22+8n。
  5. 第 6 行,立即数 -16 的补码为 0xFFFFFFFFFFFFF0(假设用 5 位表示,则模为 32,32 - 16 = 16,因此其补码为 1 0000,对于补码高位用符号位扩展后结果不变,从而高位全部为1),因此 %rax 的值与 -16 相与的结果是将低 4 位设置为 0,其余高位不变,例如 0xF1 (241) 到 0xFF (255) 之间的数将变为 0xF0(240),而 240 + 16 = 256,这样相当于 %rax 的值变为最大的小于该值的能被 16 整除的数。对于 8n + 22,当 n 是奇数时,结果为 8n + 8;当 n 是 偶数时,结果为 8n + 16。
  6. 第 7 行,将 %rsp 的值减去 %rax 的值后作为 %rsp 的值,即栈指针向下扩展,扩展的大小为 %rax 的值。
  7. 第 8 行,设置寄存器 %rax 的值为 7 + %rsp 的值。
  8. 第 9 行,将 %rax 的值逻辑右移 3 位,高位补 0,即将该值除以 8,去掉余数。
  9. 第 10 行,将 %r8 的值设置为 %rax 的值 * 8,该位置即为数组 p 的起始位置。
  10. 第 11 行, 将 %rcx 的值设置为 %r8 的值。

假设 n 为 5,s1 为 2065 和 n 为 6,s1 为 2064,则其他的值为:

solution 3.49

In earlier versions of x86 code, the frame pointer was used with every function call. With x86-64 code, it is used only in cases where the stack frame may be of variable size, as is the case for function vframe.

3.11 Floating-Point Code

未看
存放浮点数据的寄存器:
Media registers

Floating-point movement instructions:
Floating-point movement instructions

3.11.1 Floating-Point Movement and Conversion Operations

浮点型转换操作:
floating-point conversion operations

3.11.6 Floating-Point Comparison Operations

浮点型比较:
Floating-Point Comparison Operations

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值