计系2复习(2)流程控制,栈帧调用,数据存储

目录

  • 流程控制
    • 条件码
      • 设置条件码
      • 访问条件码
      • 条件传送
    • 条件跳转
    • 循环
      • do-while
      • while
      • for
    • switch跳转
  • 栈帧调用
    • 栈帧
    • 调用与返回
    • 参数传递
    • 栈上的局部存储
    • 寄存器保存
  • 数据存储
    • 数组
    • 结构体
      • 对齐
  • 题目
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

流程控制

该章节主要描述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操作都只改变条件码,不改变寄存器的值。
计系2复习(2)流程控制,栈帧调用,数据存储_第1张图片

访问条件码

通过set系列指令,将条件码加载到某个寄存器的某一字节(全设为0或者1),其他字节不受影响。

计系2复习(2)流程控制,栈帧调用,数据存储_第2张图片
这个好像不太常用,略过了

条件传送

条件传送根据条件码的值,如果符合则传送数据到目的寄存器,否则不动。这样也能够间接 实现类似if的跳转

计系2复习(2)流程控制,栈帧调用,数据存储_第3张图片
这个好像不太常用,略过了

条件跳转

条件跳转语句(j系列)根据条件码做出不同的跳转。

使用jmp指令可实现无视条件码,进行无条件跳转。而其他的指令需要检验条件码,比如 jg 就是 jump greater,即大于(0)就跳转

计系2复习(2)流程控制,栈帧调用,数据存储_第4张图片
跳转语句使用PC+偏移寻址,即下一条指令的起始地址,是通过pc + jump指令的操作数得到的。

比如有如下的汇编代码,可以看到pc+偏移寻址的过程:main调用top,而top调用leaf,通过pc+偏移寻址的过程。

(注意小端表示法,即 f2 ff ff ff 其实是 0xfffffff2)

计系2复习(2)流程控制,栈帧调用,数据存储_第5张图片

循环

循环是由条件跳转+判断语句实现的高级逻辑。

do-while

do-while循环先执行body-statement,再判断test-expr,通过测试语句的结果决定是否要进行跳转。

以计算n的阶乘为例,有如下的循环代码:

计系2复习(2)流程控制,栈帧调用,数据存储_第6张图片

while

while循环和do-while类似,只是在开头加多了一次判断,因为while循环可能一次都不执行:

计系2复习(2)流程控制,栈帧调用,数据存储_第7张图片
汇编实现:while转do-while即可

for

for循环和while类似,只是多了一个 update-statement(更新语句)和 init-expr(初始化表达式)。

更新语句通常在 body-statement 之后执行,而初始化语句在进入循环之前执行一次即可

计系2复习(2)流程控制,栈帧调用,数据存储_第8张图片
汇编实现:通过将for转换为while,然后while转do-while即可

switch跳转

在case跨度较小且分布较为集中时,GCC会使用跳转表来实现case的跳转。

计系2复习(2)流程控制,栈帧调用,数据存储_第9张图片

栈帧调用

栈帧调用这一部分主要描述了函数的调用过程,如何压栈,创建自己的栈帧等等。

栈帧

栈帧即运行时栈,描述了一个子程序(过程调用)中分配的独立栈空间。

在调用一个函数时,往往有如下的步骤,创建子程序的栈帧:

  1. 调用者P:将函数参数按照倒序压入栈中
  2. 调用者P:将函数返回地址压入栈中
  3. 被调用者Q:保存调用者P的栈底指针,即ebp寄存器的旧值,将ebp寄存器的旧值压入栈中,压栈的位置地址为 bottom,这个地址表示被调用者Q的栈底
  4. 被调用者Q:将ebp寄存器赋值为 bottom,表示新的栈帧底部的位置,这个过程叫做栈帧切换

计系2复习(2)流程控制,栈帧调用,数据存储_第10张图片

注:EBP即基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部 – 引自【详细解析ESP寄存器与EBP寄存器】。

调用与返回

使用 call 指令调用一个函数。call 指令会

  1. 将pc值压栈(即函数返回地址压栈)
  2. 跳转到call操作数指定的地址

而 ret 指令会从栈中弹出一个返回地址,并且使程序跳转到该返回地址。

参数传递

通过寄存器最多传递6个参数,其余的参数需要通过栈传递。

值得注意的是,前1-6个参数通过寄存器,下面给出寄存器及其对应参数列表:

计系2复习(2)流程控制,栈帧调用,数据存储_第11张图片

第 7-n 个参数通过栈,其中参数存储的顺序是:先压入第n个参数,然后 n-1,n-2 … 直到第7个参数。第7个参数存在于栈顶

此外,通过栈传递的参数,数据大小都向8的倍数对齐。

栈上的局部存储

在函数中有时会声明一些局部变量,比如寄存器不够用的情况下,或者是用到指针的情况下,就需要将局部变量的值存储在栈中。下面三种情况,会使用栈上的临时存储:

  1. 寄存器不够存放所有的本地数据
  2. 对某一个局部变量使用&(取地址)运算,因此必须能够为它产生一个地址
  3. 某些局部变量是数组或者结构体,必须使用指针+偏移的形式访问成员

如下图,因为需要用到指针(用栈指针表示变量的地址),申请16个字节的栈空间,存放两个long变量:

计系2复习(2)流程控制,栈帧调用,数据存储_第12张图片

寄存器保存

因为所有过程公用一套寄存器,所以要保证寄存器的值不被改变,通常将寄存器的值保存在栈中即可。

调用者保存与被调用者保存:
计系2复习(2)流程控制,栈帧调用,数据存储_第13张图片

数据存储

这一章节主要介绍数据如何排布在内存中,比如数组,结构体的存储。

数组

懂的都懂好吧

二维数组寻址:

int A[N][M]

需要寻址 A[i][j],有效地址为:A + 4*(i*M + j)

注意这个4,因为int是4字节的。

结构体

不考虑对齐的情况,那么直接按照顺序排布即可:
计系2复习(2)流程控制,栈帧调用,数据存储_第14张图片

对齐

数据对齐的原则是:任何数据的基本对象地址必须是 K 的倍数。值得注意的是 K 和数据类型所占用的字节数目相同。

计系2复习(2)流程控制,栈帧调用,数据存储_第15张图片
下面的例子解释了 K 及其作用。
计系2复习(2)流程控制,栈帧调用,数据存储_第16张图片
除了保证头部对齐以外,还要保证尾部对齐。因为可能会遇到结构体数组的情况。

尾部不对齐会导致下一个元素的头部不对齐,故尾部也要执行对齐操作。

计系2复习(2)流程控制,栈帧调用,数据存储_第17张图片

题目

1

写出下面函数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;
}

2

对于数组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

3

考虑如下的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; 
}

4

寄存器%cl和%rax内存放的值分别为

%cl=0xF3 
%rax=0x0000000000000001,

则执行指令salb %cl, %rax 后寄存 器%rax内存放的值(十进制)大小为:__

0xF3 = -13,salb即左移 -13 位,那么相当于右移 13 位,sa是补上符号位的移位,于是有 移位后:

0x0000000000001000

十进制为 8

5

定义结构struct S { int i; char c; char d;} *p[2];

则存放p[1].c的内存地址为 ___

计系2复习(2)流程控制,栈帧调用,数据存储_第18张图片
如图,为 p+12

6

定义结构struct S1 {char c; int i; char d;} *p;,其所占用的内存空间为____个字节。

A:  6     
B:  7      
C:  9     
D:  12 

答案是 D
计系2复习(2)流程控制,栈帧调用,数据存储_第19张图片
第一个char考虑int要对齐,所以占4字节,而第二个char考虑第二个结构体(结构体数组的情况)中的int的对齐。

如果第一个结构体的第二个char不补齐后面的3字节,那么第二个结构体的int无法对齐

7

已知函数Sum_fun的C语言代码及其对应的x86-64汇编代码框架,请补齐缺失的汇编代码

void Sum_fun (int *p)
{
     
	int a[4] = {
     1, 2, 4, 8}, i = 0;
	*p=0do {
     
		*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 

8

下图是一个函数的(不太好的)实现,这个函数从标准输入读入一行,将字符串复制到新分配的存储中,并返回一个指向结果的指针。

计系2复习(2)流程控制,栈帧调用,数据存储_第20张图片
计系2复习(2)流程控制,栈帧调用,数据存储_第21张图片
答:执行完第三行汇编代码,总共执行两句

push %rbx
subq $0x10, %rsp

即将rbx压栈,然后栈指针-=16,即开16字节的栈,所以有:

计系2复习(2)流程控制,栈帧调用,数据存储_第22张图片

执行 gets 之后,因为传入rsp作为数组首地址,而字符填充按照内存地址递增填充,并且这题不考虑小端表示,那么有:

计系2复习(2)流程控制,栈帧调用,数据存储_第23张图片

所以程序试图返回0x400034地址,除此之外,rbx寄存器的值被破坏了。

你可能感兴趣的:(计算机系统,指针,汇编,x86)