该章节主要描述x86中汇编代码如何实现流程控制,比如if,switch跳转,或者是for,while循环。
和LC-3中的nzp条件码类似,x86中的条件码,记录了最后一次运算操作的结果情况。
下面给出常用的条件码列表:
名称 | 全称 | 中文名 | 为1时的意义 |
---|---|---|---|
CF | Carry Flag | 进位标志 | 最近的操作使最高位产生进位 |
ZF | Zero Flag | 零标志位 | 最近的操作得出的结果为0 |
SF | Sign Flag | 符号标志位 | 最近的操作得到的结果为负数 |
OF | Overflow Flag | 溢出标志位 | 最近的操作导致一个补码溢出 |
通过jmp语句及其变式,可以通过判断条件码,来执行对应的跳转。
通过 cmp 和 test 指令可以直接设置条件码,一般用于条件跳转之前,设置对应的跳转条件。
值得注意的是:cmp指令基于 s2 - s1
,如果有
cmpq S1, S2
jg .L2
那么意思是如果 S2 - S1 > 0
就跳转到 .L2
。
而test指令常用于判断一个数是否为0,比如
test s1, s1
je .L2
意思是如果s1 & s1 = 0
(即s1 = 0),那么跳转到.L2
。
除此之外,cmp和test操作都只改变条件码,不改变寄存器的值。
通过set系列指令,将条件码加载到某个寄存器的某一字节(全设为0或者1),其他字节不受影响。
条件传送根据条件码的值,如果符合则传送数据到目的寄存器,否则不动。这样也能够间接 实现类似if的跳转
条件跳转语句(j系列)根据条件码做出不同的跳转。
使用jmp指令可实现无视条件码,进行无条件跳转。而其他的指令需要检验条件码,比如 jg 就是 jump greater,即大于(0)就跳转
跳转语句使用PC+偏移
寻址,即下一条指令的起始地址,是通过pc + jump指令的操作数
得到的。
比如有如下的汇编代码,可以看到pc+偏移寻址的过程:main调用top,而top调用leaf,通过pc+偏移寻址的过程。
(注意小端表示法,即 f2 ff ff ff
其实是 0xfffffff2
)
循环是由条件跳转+判断语句实现的高级逻辑。
do-while循环先执行body-statement,再判断test-expr,通过测试语句的结果决定是否要进行跳转。
以计算n的阶乘为例,有如下的循环代码:
while循环和do-while类似,只是在开头加多了一次判断,因为while循环可能一次都不执行:
for循环和while类似,只是多了一个 update-statement(更新语句)和 init-expr(初始化表达式)。
更新语句通常在 body-statement 之后执行,而初始化语句在进入循环之前执行一次即可
汇编实现:通过将for转换为while,然后while转do-while即可
在case跨度较小且分布较为集中时,GCC会使用跳转表来实现case的跳转。
栈帧调用这一部分主要描述了函数的调用过程,如何压栈,创建自己的栈帧等等。
栈帧即运行时栈,描述了一个子程序(过程调用)中分配的独立栈空间。
在调用一个函数时,往往有如下的步骤,创建子程序的栈帧:
注:EBP即基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部 – 引自【详细解析ESP寄存器与EBP寄存器】。
使用 call 指令调用一个函数。call 指令会
而 ret 指令会从栈中弹出一个返回地址,并且使程序跳转到该返回地址。
通过寄存器最多传递6个参数,其余的参数需要通过栈传递。
值得注意的是,前1-6个参数通过寄存器,下面给出寄存器及其对应参数列表:
第 7-n 个参数通过栈,其中参数存储的顺序是:先压入第n个参数,然后 n-1,n-2 … 直到第7个参数。第7个参数存在于栈顶。
此外,通过栈传递的参数,数据大小都向8的倍数对齐。
在函数中有时会声明一些局部变量,比如寄存器不够用的情况下,或者是用到指针的情况下,就需要将局部变量的值存储在栈中。下面三种情况,会使用栈上的临时存储:
&
(取地址)运算,因此必须能够为它产生一个地址如下图,因为需要用到指针(用栈指针表示变量的地址),申请16个字节的栈空间,存放两个long变量:
因为所有过程公用一套寄存器,所以要保证寄存器的值不被改变,通常将寄存器的值保存在栈中即可。
这一章节主要介绍数据如何排布在内存中,比如数组,结构体的存储。
懂的都懂好吧
二维数组寻址:
int A[N][M]
需要寻址 A[i][j],有效地址为:A + 4*(i*M + j)
注意这个4,因为int是4字节的。
数据对齐的原则是:任何数据的基本对象地址必须是 K 的倍数。值得注意的是 K 和数据类型所占用的字节数目相同。
下面的例子解释了 K 及其作用。
除了保证头部对齐以外,还要保证尾部对齐。因为可能会遇到结构体数组的情况。
尾部不对齐会导致下一个元素的头部不对齐,故尾部也要执行对齐操作。
写出下面函数Func1汇编代码对应的C程序,其中参数1为x,参数2为y,即 x=rdi; y=rsi
:
Func1:
cmpq %rsi, %rdi
jge .L2
leaq 3(%rsi), %rdi
jmp .L3
.L2:
leaq (%rdi,%rdi,4), %rsi
addq %rsi, %rsi
.L3:
leaq (%rdi,%rsi), %rax
ret
cmp:计算 rdi-rsi,即 x-y
jge:如果 x-y>=0 则转到 .L2,否则 rdi=rsi+3 即 x=y+3,然后转 .L3
.L2:rsi = rdi * 4 + rdi,即 y=5 * x,随后 y+=y 即 y=10x
.L3:返回 rdi+rsi。
所以如果 x>=y 那么返回 10y+y=11y,否则返回 y+y+3=2y+3
// %rdi = x, %rsi = y
long Func(long x, long y) {
if (x < y) {
x = 3 + y; // return 2y+3
} else {
y = 5*x;
y += y; // return 11y
}
return x + y;
}
对于数组int B[8][5]
,需要将B[i][j]
保存到eax中
数组起始地址B在rdi,i 保存在 rsi,j 保存在 rdx 中
请完成以下代码中的空缺
leaq ( ,%rsi , ), %rax
leaq ( , , ), %rax
movl ( , , ), %eax
对于 B[i][j],因为是int所以有如下的寻址:
B + 4 * (5*i + j)
所以先计算5i+j
,则先计算 5i,那么第一句有:
leaq (%rsi, %rsi , 4), %rax
即
rax = 4*rsi + rsi = 5*x
此时 rax = 5i,然后第二句加上 j ,形成 5i + j
leaq (%rdx, %rax, 1), %rax
即
rax = rax * 1 + rdx ,即 rax += rdx 即 rax += j
此时 rax = 5i + j,最后计算 int 的 4 字节偏移,同上加上数组起始地址 B:
movl (rdi, rax, 4), %eax
即
eax = rax * 4 + rdi = (5*i+j) * 4 + B
考虑如下的x86-64汇编代码:
// a in %rdi, n in %esi
loop:
movl $0, %ecx
movl $0, %edx
testl %esi, %esi
jle .L3
.L6:
movslq %edx,%rax
movl (%rdi,%rax,4), %eax
cmpl %eax, %ecx
cmovl %eax, %ecx
addl $1, %edx
cmpl %ecx, %esi
jg .L6
.L3:
movl %ecx, %eax
ret
上述汇编代码对应的C代码如下,请将如下的C代码空白处补充完整
int loopy(int a[], int n)
{
int i;
int x = _____;
for (i = ____________; ____________; ____________) {
if (____________) x = ____________;
}
return x;
}
观察C代码结构发现是一个循环,然后里面有一个判断,然后再分析汇编代码:
// a in %rdi, n in %esi
loop:
movl $0, %ecx // 因为edx=i所以ecx=x变量
movl $0, %edx // 根据下面推测edx=i
testl %esi, %esi // 如果n==0直接结束
jle .L3
.L6:
movslq %edx,%rax // rax=edx 因为下面推测rdx=i,所以这里有rax=i
movl (%rdi,%rax,4), %eax // eax=M[4*rax+a] 推测是数组寻址 eax=a[i]
cmpl %eax, %ecx // 测试 ecx - eax 即 ecx - a[i] 即 x-a[i]
cmovl %eax, %ecx // 如果 ecx-a[i]<0 那么 ecx=a[i] 即 x=a[i]
addl $1, %edx // edx++ 推测edx是for循环的i
cmpl %ecx, %esi // 测试 esi-ecx 即 n-ecx 即 n-x
jg .L6 // 如果esi-ecx>0就继续,即 n>ecx 即 n>x 就继续
.L3:
movl %ecx, %eax // 返回ecx
ret
所以有
int loopy(int a[], int n)
{
int i;
int x = 0;
for (i = 0; x<n; i++) {
if (x-a[i] < 0) x = a[i];
}
return x;
}
寄存器%cl和%rax内存放的值分别为
%cl=0xF3
%rax=0x0000000000000001,
则执行指令salb %cl, %rax 后寄存 器%rax内存放的值(十进制)大小为:__
0xF3 = -13,salb即左移 -13 位,那么相当于右移 13 位,sa是补上符号位的移位,于是有 移位后:
0x0000000000001000
十进制为 8
定义结构struct S { int i; char c; char d;} *p[2];
则存放p[1].c
的内存地址为 ___
定义结构struct S1 {char c; int i; char d;} *p;,其所占用的内存空间为____个字节。
A: 6
B: 7
C: 9
D: 12
答案是 氧 D
第一个char考虑int要对齐,所以占4字节,而第二个char考虑第二个结构体(结构体数组的情况)中的int的对齐。
如果第一个结构体的第二个char不补齐后面的3字节,那么第二个结构体的int无法对齐
已知函数Sum_fun的C语言代码及其对应的x86-64汇编代码框架,请补齐缺失的汇编代码
void Sum_fun (int *p)
{
int a[4] = {
1, 2, 4, 8}, i = 0;
*p=0;
do {
*p += a[i];
i = i+1;
} while(i < 4)
}
subq $0x10, %rsp
movl $0x08, 12(%rsp)
_______________________(填空)
movl $2, 4(%rsp)
movl $1, (%rsp)
movl $0, %eax
movl $0, %ecx
.L1
_______________________ (填空)
_______________________ (填空)
_______________________ (填空)
jl .L1
movl %ecx, (%rdi)
_______________________(填空)
ret
观察c代码发现其实就是累加,使用 *p 指向的int记录累加和,所以有:
其中 rdi 为第一个参数,即 *p ,此外,值得注意的是数组寻址,记得乘以4,因为int事4字节
subq $0x10, %rsp // 开栈0x10即16字节
movl $0x08, 12(%rsp) // M[rsp+12]=8 填数组 a[3]
movl $4, 8(%rsp) // M[rsp+12]=4 填数组 a[2]
movl $2, 4(%rsp) // M[rsp+12]=2 填数组 a[1]
movl $1, (%rsp) // M[rsp+12]=1 填数组 a[0] 注意首地址a=rsp
movl $0, %eax // 因为ecx为累加值所以eax只能是i
movl $0, %ecx // 由下文可知ecx为累加值
.L1
addl (%rsp, %eax, 4), %ecx // ecx += a[i] 注意 4字节的int所以乘以4
addl $0x1, %eax // eax++ (i++)
cmpl $0x4, %eax // 测试 eax-4 即 i-4
jl .L1 // eax-4<0 即 i<4 则继续
movl %ecx, (%rdi) // *p=ecx 即 累加值ecx赋值给*p
addq $0x10, %rsp // 退栈
ret
下图是一个函数的(不太好的)实现,这个函数从标准输入读入一行,将字符串复制到新分配的存储中,并返回一个指向结果的指针。
push %rbx
subq $0x10, %rsp
即将rbx压栈,然后栈指针-=16,即开16字节的栈,所以有:
执行 gets 之后,因为传入rsp作为数组首地址,而字符填充按照内存地址递增填充,并且这题不考虑小端表示,那么有:
所以程序试图返回0x400034
地址,除此之外,rbx寄存器的值被破坏了。