深入理解计算机系统——第三章 Machine-Level Representation of Programs
图解系统
程序的机器级表示(一)
程序的机器级表示(二)
程序的机器级表示(三)
程序的机器级表示(四)
深入理解计算机系统学习笔记
程序的机器级表示
深入理解计算机系统(CSAPP)全书学习笔记(详细)
深入理解计算机系统 课程视频
汇编语言各种指令的解释与用法
cmov条件传送指令
第18章-x86指令集之常用指令
3.2 Program Encoding
C 程序从源文件生成可执行文件的过程:
-
The
C Preprocessor
expands the source code to include any files specified with#include
commands and to expand anymacros
, specified with#define
declarations. -
The compiler generates assembly code versions of the source files (*.s).
-
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.
-
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
-
The format and behavior of a machine-level program is defined by the
instruction set architecture
, orISA
, defining the processor state, the format of the instructions, and the effect each of these instructions will have on the state. -
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.
指令集介绍:
虚拟地址介绍:
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.”
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.
整数寄存器,用来存放整型
和指针
,有6个寄存器用来存放6个参数,如果参数超过6个,则放在栈
上。
3.4.1 Operand Specifiers
x86-64
支持操作数形式如下:
源操作数有三种:立即数(常数),寄存器,内存
目标操作数:寄存器或者内存; 结果存放的位置
立即数(immediate):常数值。
内存引用(memory reference):根据有效地址获取内存的位置。
-
对于 ATT 形式的汇编代码,用
$
跟着一个整型(C 标准格式)表示立即数,如,:$-577
,$0x1F
,因此指令$-577
表示常数-577
。 不同的指令允许的立即数的范围不同。 -
用 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 为数组的索引。
-
内存被看作一个大的字节数组,因此用 M b [ A d d r ] M_b[Addr] Mb[Addr] 表示对内存中起始地址为
Addr
的b
字节的数值的引用。这里去掉下标b
。 -
M [ I m m ] M[Imm] M[Imm] 是绝对寻址,直接给内存的地址(立即数),指令的格式为一个立即数
Imm
。 -
M [ R [ r a ] ] M[R[r_{a}]] M[R[ra]] 是间接对寻址,获取寄存器 r a r_{a} ra 的值作为内存的其实地址,指令的格式为 ( r a ) (r_{a}) (ra)。
-
对于指令格式 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。
示例:
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
字节:
示例:
指令 | 解释 |
---|---|
movl $0x4050,%eax | Immediate–Register, 4 bytes |
movw %bp,%sp | Register–Register, 2 bytes |
movb (%rdi,%rcx),%al | Memory–Register, 1 byte |
movb $-17,(%esp) | Immediate–Memory, 1 byte |
movq %rax,-12(%rbp) | Register–Memory, 8 bytes |
MOV
指令源操作数和目标操作数不能同时为内存,因为将数据从一个内存地址复制到另一个内存地址需要两条指令完成,需要先将源地址的数据放到一个寄存器中,再从该寄存器的数值写到目标内存地址。- 指令和所用寄存器大小需匹配,如用
movl
时,寄存器用 32 位寄存器。 - 通常,
mov
指令只会根据目标操作数来更新寄存器中的特定位,如用movabsq
将立即数复制到64位寄存器中,则目标寄存器的 64 位都用到,更新数据,源数据没有 64 位,则高位用 0 填充;而对于movw
指令,目标寄存器位 16 位,因此数据只更新低 16 位,高位不更新;但movl
是一个特例,更新低 32 位后,会将高 32 位填充为 0。见下面例子,第一次用movabsq
指令后更新%rax
的全部 64 位数,之后用movb
指令,目标寄存器是%al
,即%rax
寄存器的低 8 位,源操作数 -1 的补码为 FF,因此低 8 位更新为 FF,其他高位不变:
- 当用
movq
指令且源操作数为立即数时,立即数只能是 32 位的补码,而不能是 64 位,高位将用符号位扩展从而得到 64 位(第二章介绍过符号位扩展补码的高位,其结果不变)。 - 用
movabsq
指令时,源操作数可以是任意的 64 位立即数,目标操作数只能是寄存器。
当目标操作数尺寸大于源操作数,可以在高位进行零扩展
或者符号位扩展
。
零扩展 movz
系列指令:剩下的高位填充 0。
注意:这里没有 movzlq
,因为当使用 movl
指令时,且目标操作数为寄存器,那么寄存器的高4个字节会填充 0
。
要求:源操作数为寄存器或者内存地址,目标操作数位寄存器。
符号位扩展 movs
系列指令:剩下的高位用符号位填充:
示例:
第一个参数 xp 和第二个参数 y 分别存在寄存 %rdi
和 %rsi
中。
指令 2:根据 xp
地址从内存中取数值,存到寄存器 %rax
中。
指令3:将 %rsi
的值,即 y
的值存到地址为 xp
的内存中。
最后将寄存器 %rax
的值,即 x
作为返回值返回。
上述的局部变量保存在寄存器而非内存中,寄存器比内存的访问速度更快。
示例1:
根据图 3.2 可知所用寄存器大小,题解中说对于 x86-64,内存引用总是 quad word。
示例2:
示例3:
- 因为参数类型为指针,用 8 字节寄存器。
- 根据图 3.2 可知,第一个到第三个参数分别为 %rdi,%dsi 和 %rdx,三个寄存器的值分别为 xp,yp,和 zp,为地址。
- 第一个指令源操作数为内存引用,内存地址为 xp,从内存地址 xp 处取数值,存放到寄存器 %r8 中,用 x 代表 %r8 的值,则该指令对应
x = *xp
。 - 第二个和第三个指令同第一个类似,分别用 y 和 z 代表寄存器 %rcx 和 %rax 的值,则这两条指令分别为
y = *yp
和z = *zp
。 - 第四条指令源操作数 %r8 的值,即 x,目标操作数为内存地址 yp,则将 x 存放到内存地址为 yp 处,即
*yp = x
。 - 第五条指令和第六条指令同第四条指令类型,分别为
*zp = y
和*xp = z
。
因此对应的 C 代码如下:
3.4.4 Pushing and Popping Stack Data
-
栈 (stack)是一种数据结构,遵循先进后出的规则,可以增加(push)或删除(pop)数据。
-
栈可以由数组来实现,数组的末尾为栈顶,添加或删除元素在栈顶操作。
-
程序用栈来管理过程调用与返回的状态。
-
对于
×86-64
,栈存在内存的某个区域,且向下增长,即栈底在高位地址处,当 push 指令添加数据时,栈顶向低位扩展,栈指针%rsp
指向栈顶位置。 -
当使用
pop
弹出栈顶的数据时,实际数据仍在该地址,只是栈顶的指针指向的位置变了(向高地址移动8)。
Push
和 Pop
指令:
栈操作介绍:
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
指令的一个变体,从内存读数据到寄存器,但不是内存引用,而是复制内存地址到寄存器。
例如:%rax 的值为 x,则指令 leaq 7(%rdx,%rdx,4), %rax
表示将寄存器 %rax 的值设为 7 + 5x,这个过程只复制内存地址而不访问内存; 而如果是 movq
指令,则是将内存地址为 7 + 5x 的值设置为寄存器 %rax 的值。
示例:
- 第一个参数 x 存在寄存器 %rdi 中,第二个参数 y 存在寄存器 %rsi 中,第三个参数 z 存在寄存器 %rdx 中。
- 第一个指令源操作数为地址 10 * y,leaq 指令只复制地址而不是同 mov 指令一样访问内存,因此该指令将 %rbx 的值设置为 10 * y。
- 第二个指令将 %rbx 设置为 10 * y + z。
- 第三个指令将 %rbx 设置为 10 * y + z + x * y。
- 最后返回为 %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 指令相同,两个操作数不能同时为内存地址。
当第二个操作数为内存地址时,处理器从内存地址读数据,执行运算操作后,将结果写回到该内存地址。
示例:
答案如下:
- 第一条指令,源操作数为寄存器 %rcx 的值 0x1,目标操作数为内存地址 0x100 (寄存器 %rax 的值),将 0x 100 的值设置为 0xFF ( 初始内存中的值) + 0x01 (%rcx 的值) = 0x100。
- 第二条指令,目标操作数为内存地址 8 + 0x100 = 0x108,执行减法指令:0xAB(内存地址为 0x108 的值) - 0x3(%rdx 的值)= 0xA8,最后将计算结果写回到内存地址 0x108 处。
- 第三条指令,目标操作数为内存地址 0x100 + 0x3 * 8 = 0x118,执行乘法指令:0x10 (立即数 16)* 0x11(内存地址为 0x118 的值) = 0x110,最后将计算结果 0x110 写回内存地址 0x118 处。
- 第四条指令,目标操作数为内存地址 0x100 + 0x10(立即数 16) = 0x110,从该地址取值 0x13,将加减1,得到 0x14,然后写回内存地址 0x110 处。
- 第五条指令,两个操作数均为寄存器,目标操作数为寄存器 %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。
示例:
3.5.4 Discussion
图 3.10 中的指令能用于无符号数或者补码计算,有符号和无符号只有右移运算有区别,第二章有介绍相关知识。
示例 1:
上面计算第二个乘法语句 z * 48 使用了两条指令,先计算 3 * z,然后左移 4 位,因为左移 1 位相当于乘以 2,因此结果位 3 * z * 16 得到 z * 48。
示例2:
示例3:
- 该异或的指令可以将寄存器的值设置为 0,相同的位异或后为0,因此将
%rcx
的值与自己异或后为0,然后将结果 0 设置为%rcx
的值。 - 可以直接设置该值为 0:
movq $0, %rcx
。 - 没明白,答案说任何指令更新低位的 4 字节会造成高位 4 字节为 0 (??没明白),因此异或时只用处理低 4 字节,用 xorl %ecx,%ecx;或者用 movl $0, %ecx,因为 movl 会将高位设置为 0 (前面有讲)。
3.5.5 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
指令外都会改变下面的某些条件代码寄存器的值。
CF: 进位标志
比较和测试指令:
汇编语言CMP(比较)指令:比较整数
汇编语言TEST指令:对两个操作数进行逻辑(按位)与操作
汇编语言各种指令的解释与用法
比较指令:如果相等则设置 ZF标志位为 1。
测试指令:和 AND
指令使用相同,但不会修改内容(前面可知 AND 指令会将计算结果更新到目标操作数中)。
3.6.2 Accessing the Condition Codes
SET
指令:
SET
的目标操作数:图 3.2 中最右侧低位 1 字节的寄存器,或者单字节的内存地址,其结果为 0 或者 1。
如果想得到 32 位 或者 64 位 的结果,需3.9要将高位清零。
3.6.3 Jump Instructions
jump
指令:
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.
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
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.
条件传送指令无需预测,效率更高。
条件传送指令:
条件转移示例:
如果要做如下判断:
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-while
,while
和 for
通过条件判断和跳转实现循环控制。
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
指令中),该指令结合了 jump
和 push
的功能。
当被调用的过程 Q
执行完成,会执行 ret
指令(或 retq
),该指令就是逆转 call
指令的效果。它会假设栈顶有一个想要跳转的地址,然后将栈顶的地址弹出(pop
指令,弹出后栈顶的指针会增加,而该地址的内容不会消失,只是不属于栈的一部分),然后将程序计数器设置为弹出的地址,因此程序会回到原来的地方继续执行。
2、传递数据 Passing data
存放参数的寄存器有6个:%rdi, %rsi, %rdx, %rcx, %r8, %r9
,存放返回值的寄存器位 %rax
。
上述这些寄存器只能存放整型和指针,如果参数超过6个,则参数会被放入栈中。(参数放在寄存器中比栈中访问速度更快)
3、管理和释放内存 Allocating and deallocation memory
被调用的函数 Q
必须为局部变量分配空间,并且在返回前释放存储空间。
栈结构:
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
和 %r11
为 Caller Saved
寄存器,用于存放任何可以被函数
修改的临时值。
寄存器 %rbx, %rbp
和 %r12–%r15
为 Callee Saved
寄存器,当一个函数要改变这些寄存器的值时,必须先压入栈中保存再在返回时从栈中弹出恢复数据
示例:
上述代码使用 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
L⋅N,其中 L
为数组元素类型 T
的大小(bytes),N
为数组元素的个数。
标志符 A
将被用作一个指向数组起始地址的指针,数组元素的索引在 0
到 N-1
区间,第 i
个元素的索引为
x
A
+
L
⋅
i
x_{A} + L \cdot i
xA+L⋅i。
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+L⋅i, where
L
L
L is the size of data type
T
T
T .
示例:
上述例子返回值的存放:the result being stored in either register %eax
(for data) or register %rax
(for pointers).
述返回结果为 int
类型的数组元素值时,用 movl
和寄存器 %eax
,而返回值为 int *
指针时,为 leaq
(leaq
指令不是引用内存而是复制地址)和寄存器 %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(C⋅i+j)
其中 L
是数据类型 T
的大小(bytes)。
数组的结构如下图:
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];
优化后的汇编代码如下:
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(C⋅i+j),可得到
x
A
+
64
i
x_{A} + 64i
xA+64i,而 i
左移 6
位即为
i
∗
2
6
i * 2^6
i∗26。
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]
前声明,汇编代码如下:
这里计算
n
⋅
i
n \cdot i
n⋅i 用到 imulq
乘法指令,而非用左移指令。
示例:
优化后的汇编代码:
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;
};
其结构如下:
可见数组是嵌入在结构体中。
例如变量 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+4⋅1=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
数据对齐:基于硬件的需求
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:
具体 K
的大小是多少,由结构体中尺寸最大的类型对应的 K
决定。
如:
struct S1
{
int i;
char c;
int j;
};
对齐后如下,第二个数据 c 会填充 3 个字节来满足对齐:
有时编译器可能需要在结构体末尾填充,如:
struct S2
{
int i;
int j;
char c;
};
对齐后:
3.10 Combining Control and Data in Machine-Level Programs
3.10.3 Out-of-Bounds Memory References and Buffer Overflow
GDB
调试的命令:
3.10.4 Thwarting Buffer Overflow Attacks (?)
未看
缓冲区溢出解决方案:
- Stack Randomization
- Stack Corruption Detection
- 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
).
栈帧的结构如下:
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
.
汇编代码如下:
- 第 2 行,保存当前
%rbp
寄存器的值到栈中。 - 第 3 行,将
%rbp
寄存器的值设置为栈指针位置%rsp
。 - 第 4 行,将栈指针位置向下扩展16 字节,前 8 个字节存放局部变量 i,后 8 个字节未使用(unused)。
- 第 5 行,将寄存器
%rax
的值设置为 22+8n。 - 第 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。 - 第 7 行,将
%rsp
的值减去%rax
的值后作为%rsp
的值,即栈指针向下扩展,扩展的大小为%rax
的值。 - 第 8 行,设置寄存器
%rax
的值为 7 +%rsp
的值。 - 第 9 行,将
%rax
的值逻辑右移 3 位,高位补 0,即将该值除以 8,去掉余数。 - 第 10 行,将
%r8
的值设置为%rax
的值 * 8,该位置即为数组 p 的起始位置。 - 第 11 行, 将
%rcx
的值设置为%r8
的值。
假设 n 为 5,s1 为 2065 和 n 为 6,s1 为 2064,则其他的值为:
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
未看
存放浮点数据的寄存器:
Floating-point movement instructions:
3.11.1 Floating-Point Movement and Conversion Operations
浮点型转换操作:
3.11.6 Floating-Point Comparison Operations
浮点型比较: