CSAPP 第三章家庭作业

CSAPP 第三章家庭作业

参考了

《深入理解计算机系统》第三版]: https://blog.csdn.net/weixin_47089544/article/details/124302414
深入理解计算机系统(第三版)作业题答案(第三章)]: (https://www.cnblogs.com/machao/p/8471809.html

深入理解计算机系统: http://doraemonzzz.com/2021/04/08/深入理解计算机系统 第3章 习题解析 Part1/
CSAPP-3e-Solutions/chapter3/3.59: https://dreamanddead.github.io/CSAPP-3e-Solutions/chapter3/3.59/
非常感谢

3.58 一个函数原型为decode2(long x,long y,long z)

题目

long decode2(long x,long y,long z);

GCC产生如下汇编代码:

1 decode2:
2	subq	%rdx,%rsi
3	imulq	%rsi,%rdi
4	movq	%rsi,%rax
5	salq	$63,%rax
6	sarq	$63,%rax 
7	xorq	%rdi,%rax
8	ret

参数x、y和z通过寄存器%rdi、%rsi%rdx传递。代码将返回值存放在寄存器%rax中。
写出等价于上述汇编代码的decode2的C代码。

过程

先看看汇编代码:

1 decode2:
2	subq	%rdx,%rsi//y=y-z
3	imulq	%rsi,%rdi//x=x*y
4	movq	%rsi,%rax//res=y
5	salq	$63,%rax//res<<=63
6	sarq	$63,%rax //res>>=63
7	xorq	%rdi,%rax//res=res^x
8	ret//return res

得到

decode2(long x,long y,long z){
    long res=y-z;
    return (res*x)^(res<<63>>63);
}

3.59 stored_prod计算两个64位有符号值x和y 的 128位乘积

题目

下面的代码计算两个64位有符号值x和y 的 128位乘积,并将结果存储在内存中:

typedef __int128 int128_t;
void store_prod(int128_t *dest,int64_t x,int64_t y) {
	*dest =  x* (int128_t) y;
}

GCC产出下面的汇编代码来实现计算:

1 store_prod :
2	movq	%rdx,%rax
3	cqto
4	movq	%rsi,%rcx
5	sarq	$63,%rcx
6	imulq	%rax,%rcx
7	imulq	%rsi,%rdx
8	addq	%rdx,%rcx
9	mulq	%rsi
10	addq	%rcx,%rdx
11	movq	%rax,(%rdi)
12	movq	%rdx,8(%rdi)
13	ret

为了满足在64位机器上实现128位运算所需的多精度计算,这段代码用了三个乘法。描述用来计算乘积的算法,对汇编代码加注释,说明它是如何实现你的算法的。提示:在把参数x和y扩展到128位时,它们可以重写为

\[x=2^{64}\cdot x_h+x_l\\ y=2^{64}\cdot y_h+y_l\\ \]

这里xh,xl,yh,yl都是64位值。类似地,128位的乘积可以写成

\[p=2^{64}\cdot p_h + p_l \]

这里ph和pl是64位值。请解释这段代码是如何用xh,xl,yh,yl来计算ph和pl的。

过程

需要看P1333.5.5 特殊的算术操作,介绍了汇编代码中imulqmulq等指令,和题目中的函数。

下标h和l表示高位(符号位)和低位,之所以可以这么写是因为符号拓展,以4位二进制int为例:
1111的补码数,为-1.将其进行符号拓展后为1111 1111,其值也为-1,但这里可以将1111 1111写为高位1111的补码数 *2^4 + 低位1111的无符号数:
即-1 *2^4 + 15 = -1.

原理:%rdx和%rax的二进制连起来表示这个数,既然连起来了,符号位就跑到了%rdx的最高位了,除符号位权值为负外,其余位的权值均为正。所以,高位寄存器%rdx当做补码数,低位寄存器%rax当做无符号数。因为符号位现在在高位寄存器那儿呢,所以高位寄存器当做补码数了;而低位寄存器的每一位的权值现在都是正的了,所以低位寄存器要当做无符号数。

首先看公式,

\[\begin{aligned} x \times y & = 扩展到128位后的x与y相乘\\ &=\left(2^{64} \cdot x_{h}+x_{l}\right) \times \left(2^{64} \cdot y_{h}+y_{l}\right)\\ &= 2^{128}. (x_h y_h) + 2^{64}.(x_h y_l + x_l y_h) + x_l y_l \\ &第一项2^{128}. (x_h y_h)溢出,截断后全为0,忽略\\ &第二项,(x_hy_l+x_ly_h)这个数值 是需要放在高位寄存器中的\\&(因为这一项乘以的数为2^{64},假设x_hy_l 分别是-1和UMAX, \\&仅仅是它俩的乘积都会使得高位寄存器溢出(考虑补码数和无符号数的表示范围就能想到),如果溢出,放入高位寄存器时会自行截断。 \\&第三项x_ly_l直接使用双寄存器来保存结果. \end{aligned} \]

然后看汇编代码

//dest in %rdi, x in %rsi, y in %rdx
1 store_prod:
2     movq   %rdx, %rax   # %rax = y
3     cqto                #转换q为16字节长,即4字符号拓展到8字,假如y的符号位为1,那么%rdx所有位都是1(此时值是-1),否则,%rdx全为0(此时值是0).%rdx = yh
4     movq   %rsi, %rcx   # %rcx = x
5     sarq   $63,  %rcx   # 将%rcx向右移63位,跟%rdx的含义一样,二进制位要么全是1,要么是0,%rcx = xh.
6     imulq  %rax, %rcx   # %rcx = y * xh
7     imulq  %rsi, %rdx   # %rdx = x * yh
8     addq   %rdx, %rcx   # %rcx = y * xh + x * yh,计算了第二项
9     mulq   %rsi         # 无符号计算 xl*yl,mulq命令会将xl*yl的128位结果的高64位放在%rdx,低位放在%rax,计算了第三项.
10    addq   %rcx, %rdx   # 将第二项计算结果加到%rdx
11    movq   %rax, (%rdi) # 将%rax的值放到dest的低位
12    movq   %rdx, 8(%rdi)# 将%rdx的值放到dest的高位
13    ret

重点讲一下6-8行,发现这里代码计算的是

\[(x_hy+xy_h) \]

而数学公式要求的不一样,之所以汇编要如此计算,是利用了相同的位级向量,无论用无符号数乘法还是补码乘法,其结果的截断的位级表示肯定是一样的。

但这里有点不一样,给定x'和y'两个位级向量,固定将x'看作补码数,而将y'分别看作补码数和无符号数,那么x与y的两种乘积的截断的位级表示是一样的。接下来用个小例子来证明该结论。(注意代码是将乘积的截断的位级表示看作补码数的)

假设整数类型为3位,x'和y'分别为111111,x的值为-1,而y的值分别为-1,7.
首先看-1 * -1 = 1,那么位级表示为001
再看-1 * 7 = -7,那么位级表示为1001,截断后为001
证毕。

考虑下第9行是否会溢出,

\[无符号数最大为2^{64}−1,所以两个无符号数的乘积最大为(2^{64}-1)^2 \\ 等于2^{128}+1-2^{65}.而128位的补码数的最大范围为2^{127}-1,\\ 而(2^{128}+1-2^{65})-(2^{127}-1) = 2^{127}+2-2^{65} > 0,所以可能溢出。 \]

3.60 考虑下面的汇编代码

题目

long loop(long x, int n) x in % rdi,n in %esi

loop:
	movl	%esi,%ecx
	movl	$1,% edx
    mov1	$0,% eax
	jmp	.L26
.L3:
	movq	%rdi,%r8
    andq	%rdx,%r8
    orq		%r8,%rax
	salq	%c1,%rdx
.L2:
	testq	%rdx,%rdx
	jne	.L3
    rep; ret

以上代码是编译以下整体形式的C代码产生的:

long loop(1ong x,int n)
{
	long result=___;
	long mask;
    for(mask=__;mask__;mask=__){
		result |=____;
	}
	return result;
}

你的任务是填写这个C代码中缺失的部分,得到一个程序等价于产生的汇编代码。回想一下,这个函数的结果是在寄存器%rax中返回的。你会发现以下工作很有帮助:检查循环之前、之中和之后的汇编代码,形成一个寄存器和程序变量之间一致的映射。
A.哪个寄存器保存着程序值x、n、result和mask?
B.result和mask的初始值是什么?
C.mask的测试条件是什么?
D.mask是如何被修改的?
E.result是如何被修改的
F.填写这段C代码中所有缺失的部分。

过程

首先看汇编代码x in %rdi,n in %esi

loop:
	movl	%esi,%ecx	#%ecx放入n
	movl	$1,%edx		#%edx放入1
    mov1	$0,%eax		#%eax放入0
	jmp	.L2				#跳转到标号L2
.L3:					
	movq	%rdi,%r8	#%r8放入x
    andq	%rdx,%r8	#%r8=%r8&%rdx,即&%edx中的1,相当于判断x最低位是1还是0
    orq		%r8,%rax	#%rax=%rax|%r8,即%eax中的0与%r8或运算,若x最低位是1得1,否则0
	salq	%c1,%rdx	#%rdx<<=%rcx的字节(%c1为%rcx最低8位,即n的低8位),即
.L2:					#
	testq	%rdx,%rdx	#检验%rdx&%rdx
	jne	.L3				#jne即不等/ZF非零时跳转,即%rdx非零就跳转到标号L3
    rep; ret			

因为x in %rdi,n in %esi,那么判断结束的部分使用了%rdx,所以mask存放在%rdx中,而返回的result肯定在%rax中,所以得到result初始值为0,而mask为1,判断结束部分是非零时跳转,所以判断条件为mask!=0,而循环修改值是将mask左移n位,汇编代码中低8位是为了保护位移,接下来是result,是mask先与%r8中的x与运算,得到结果与%rax进行或运算。

long loop(long x,int n)
{
	long result=0;
	long mask;
    for(mask=1;mask!=0;mask=mask<<n){
		result |=mask&x;
	}
	return result;
}

3.61 修改cread_alt

题目

在3.6.6节,我们查看了下面的代码,作为使用条件数据传送的一种选择:

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

我们给出了使用条件传送指令的一个尝试实现,但是认为它是不合法的,因为它试图从一个空地址读数据。
写一个C函数cread_alt,它与cread有一样的行为,除了它可以被编译成使用条件数据传送。当编译时,产生的代码应该使用条件传送指令而不是某种跳转指令。

过程

条件表达式和赋值的通用形式:v=test-expr?then-expr:else-expr;,基于条件传送的代码会对then-exprelse-expr都求值,可抽象为如下:

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

所以cread函数编译后得到:

# long cread(long *xp)
# xp in %rdi
cread:
  movq (%rdi), %rax	#v=then-expr,即使判断条件为false此行还是间接引用了空指针
  testq %rdi, %rdi	#检验t
  movl $0, %edx		#ve=0
  cmove %rdx, %rax	#若t==0,进行传送操作,v=ve
  ret				#return v

为了避免对空指针的间接引用,将判断条件改为对xp求反,规避then-expr,else-expr中对空指针的计算。

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

得到如下汇编:

# long cread_alt(long* xp)
# xp in %rdi
cread_alt:
  movl $0, %eax		#v=then-expr
  testq %rdi, %rdi	#检验!t
  cmovne (%rdi), %rax#若!t==0,即t不为空指针,进行传送操作,v=ve,得到*xp

3.62 填写switch3中缺失的部分

题目

下面的代码给出了一个开关语句中根据枚举类型值进行分支选择的例子。回忆一下,C语言中枚举类型只是一种引入一组与整数值相对应的名字的方法。默认情况下,值是从0向上依次赋给名字的。在我们的代码中,省略了与各种情况标号相对应的动作。

/*Enumerated type creates set of constants numbered 0 and upward*/
typedef enum{MODE_A,MODE_B,MODE_C,MODE_D,MODE_E} mode_t;

long switch3(long *p1,long *p2,mode_t action)
{
	long result=0;
    switch(action){
		case MODE_A:
            
		case MODE_B:
            
		case MODE_C:
            
		case MODE_D:
            
		case MODE_E:
            
        default:
            
    	}
		return result;
}

产生的实现各个动作的汇编代码部分如图3-52所示(即下面的汇编代码)。注释指明了参数位置,寄存器值,以及各个跳转目的的情况标号。

#p1 in %rdi, p2 in %rsi, action in %edx
.L8:								 #MODE_E
	movl	$27,%eax
	ret
.L3: 								 #MODE_A
	movq	(%rsi),%rax
	movq 	(%rdi),%rdx
	movq	 %rdx,(%rsi)
	ret
.L5: 								 #MODE_B
	movq	(%rdi),%rax
	addq	(%rsi),%rax
	movq 	%rax,(%rdi)
	ret
.L6:  								 #MODE_C
	movq 	$59,(%rdi)
	movq	(%rsi),%rax
	ret
.L7:  								 #MODE_D
	movq	(%rsi),%rax
	movq	 %rax,(%rdi)
	mov1	$27,%eax
	ret
.L9:  								 #default
	movl	$12,%eax
	ret

图3-52 家庭作业3.62的汇编代码。这段代码实现了switch语句的各个分支。

填写C代码中缺失的部分。代码包括落入其他情况的情况,试着重建这个情况。

过程

首先看汇编代码

#p1 in %rdi, p2 in %rsi, action in %edx
.L8:								 #MODE_E
	movl	$27,%eax	#%eax=27
	ret
.L3: 								 #MODE_A
	movq	(%rsi),%rax	#%rax=*p2,将%rsi中的内容当作地址寻址,p2寻址找到p2指向的值
	movq 	(%rdi),%rdx	#%rdx=*p1,和上面类似
	movq	 %rdx,(%rsi)#*p2=%rdx,即*p2=*p1
	ret
.L5: 								 #MODE_B
	movq	(%rdi),%rax #%rax=*p1
	addq	(%rsi),%rax #%rax=*p1+*p2
	movq 	%rax,(%rdi) #*p1=*p1+*p2
	ret
.L6:  								 #MODE_C
	movq 	$59,(%rdi) #*p1=59
	movq	(%rsi),%rax#%rax=*p2
	ret
.L7:  								 #MODE_D
	movq	(%rsi),%rax #%rax=*p2
	movq	 %rax,(%rdi)#*p1=*p2
	mov1	$27,%eax	#%rax=27
	ret
.L9:  								 #default
	movl	$12,%eax	#rax=12
	ret

接下来补齐代码

/* Enumerated type creates set of contants numbered 0 and upward */
typedef enum { MODE_A, MODE_B, MODE_C, MODE_D, MODE_E } mode_t;

long switch3(long *p1, long *p2, mode_t action) {
  long result = 0;
  switch(action) {
    case MODE_A:
      result = *p2;
      *p2 = *p1;
      break;
    case MODE_B:
      *p1 = *p1 + *p2;
      result = *p1;
      break;
    case MODE_C:
      *p1 = 59;
      result = *p2;
      break;
    case MODE_D:
      *p1 = *p2;
      result = 27;
      break;
    case MODE_E:
      result = 27;
      break;
    default:
      result = 12;
      break;
  }
  return result;
}

3.63 利用跳转表+汇编逆向switch语句

题目

这个程序给你一个机会,从反汇编机器代码逆向工程一个switch语句。在下面这个过程中,去掉了switch语句的主体:

long switch_prob(long x,long n){
	long result=x;
    switch(n){
		/*Fill in code here*/
	}
	return result;
}

图3-53(即最后面的反汇编代码)给出了这个过程的反汇编机器代码。
跳转表驻留在内存的不同区域中。可以从第5行(40059a: ff 24 f5 f8 06 40 00 jmpq *0x4006f8(,%rsi,8))的间接跳转看出来,跳转表的起始地址为0x4006f8。用调试器GDB,我们可以用命令x/6gx 0×4006f8来检查组成跳转表的6个8字节字的内存。GDB打印出下面的内容:

(gdb) x/6gx 0×4006f8
0x4006f8:	0X00000000004005a1	0x00000000004005c3
0x400708:	0X00000000004005a1	0x00000000004005aa
0x400718:	0x00000000004005b2	0x00000000004005bf

用C代码填写开关语句的主体,使它的行为与机器代码一致。

#long switch_prob(long x,long n)
#x in %rdi,n in %rsi
0000000000400590	<switch_prob>:
	400590:	48 83 ee 3c				sub		$0x3c,%rsi
	400594:	48 83 fe 05				cmp		$0x5,%rsi
	400598:	77 29 					ja 		4005c3 <switch_prob+0x33>
	40059a:	ff 24 f5 f8 06 40 00	jmpq	*0x4006f8(,%rsi,8)
	4005a1:	48 8d 04 fd 00 00 00	lea 	0x0(,%rdi,8),%rax
	4005a8:	00
	4005a9:	c3						retq
	4005aa:	48 89 f8 				mov		%rdi,%rax
	4005ad: 48 c1 f8 03 			sar 	$0x3,%rax
	4005b1: c3						retq
	4005b2:	48 89 f8				mov		%rdi,%rax
	4005b5: 48 c1 e0 04				shl		$0x4,%rax
	4005b9: 48 29 f8 				sub		%rdi,%rax
	4005bc: 48 89 c7				mov		%rax,%rdi
	4005bf: 48 Of af ff				imul	%rdi,%rdi
	4005c3: 48 8d 47 4b				lea		0x4b(%rdi),%rax
	4005c7: c3						retq

过程

如果忘记就回看P139和P160,先看反汇编代码

#long switch_prob(long x,long n)
#x in %rdi,n in %rsi
0000000000400590	<switch_prob>:
	400590:	48 83 ee 3c				sub		$0x3c,%rsi 	#%rsi=%rsi-60,即n=n-60,得到index
	400594:	48 83 fe 05				cmp		$0x5,%rsi	#比较n(index)和5
	400598:	77 29 					ja 		4005c3 <switch_prob+0x33>#n(index)>5就跳转到default:4005c3,也即0x400590(<switch_prob>)+0x33得到的0x4005c3
	#n<=5由跳转表处理
	
	40059a:	ff 24 f5 f8 06 40 00	jmpq	*0x4006f8(,%rsi,8)#间接跳转到0x4006f8 + 8*n
	# 跳到跳转表对应的位置,从跳转表来看,n(index)和5的取值只能是0-5,因为只有6个八字节
	
	# 先分析哪些情况会跳到4005a1,查跳转表发现有两个4005a1,即下面两种情况
	# (1)index=0,0x4006f8加上偏移量0不变,所以取出前4四字节存的0X4005a1;
	# (2)index=2时,0x4006f8加上偏移量16(十进制)得到0x400708,所以取出前4四字节存的0X4005a1;
	4005a1:	48 8d 04 fd 00 00 00	lea 	0x0(,%rdi,8),%rax #%rax=0x0+8*x
	4005a8:	00
	4005a9:	c3						retq		
    
    #哪些情况会跳到4005aa,查表只有一种,是0x400708后四字节存储的,所以
    #	index=3时,0x4006f8加上偏移量24得到0x400708+8,取出后4四字节存的0X4005aa
	4005aa:	48 89 f8 				mov		%rdi,%rax	#%rax=x
	4005ad: 48 c1 f8 03 			sar 	$0x3,%rax	#%rax=x>>3
	4005b1: c3						retq
	
	#查表得到0x400718:
	# 	index=4,0x4006f8加上偏移量32得到0x400718,取出前4四字节存的4005b2
	4005b2:	48 89 f8				mov		%rdi,%rax	#%rax=x
	4005b5: 48 c1 e0 04				shl		$0x4,%rax	#%rax=x<<4
	4005b9: 48 29 f8 				sub		%rdi,%rax	#%rax=%rax-x
	4005bc: 48 89 c7				mov		%rax,%rdi	#x=x<<4-x
	
	#注意前一行没有retq,所以这里跟index=4直接没有break,查表得到0x400718后四个字节
	# 	index=5
	4005bf: 48 Of af ff				imul	%rdi,%rdi	#x=x*x
	
	#	default(index>5,即n>60+5),同时查表发现index=1时,也会到4005c3
	4005c3: 48 8d 47 4b				lea		0x4b(%rdi),%rax#%rax=0x4b+x=75+x
	4005c7: c3						retq

然后填上switch

long switch_prob(long x,long n){
	long result=x;
    switch(n){
		/*Fill in code here*/
        case 60:
        case 62:
        	result=8*x;
            break;
        case 63:
            result=x>>3;
            break;
        case 64:
            result=(x<<4)-x;
        case 65:
            x=x*x;
        default:
            result=x+75;
	}
	return result;
}

3.64 store_ele

题目

考虑下面的源代码,这里R、S和T都是用#define声明的常数:

long A[R][S][T];

long store_ele(long i,long j,long k,long *dest)
{
	*dest=A[i][j][k];
	return sizeof(A);
7}

在编译这个程序中,GCC产生下面的汇编代码:

#long store-ele(long i,long j,long k,long*dest)
#i in %rdi,j in %rsi,k in %rdx,dest in %rcx
store_ele:
	1eaq	(%rsi,%rsi,2),%rax
	leaq	(%rsi,%rax,4),%rax
	movq	%rdi,%rsi
	salq 	$6,%rsi
	addq	%rsi,%rdi
	addq	%rax,%rdi
	addq	%rdi,%rdx 
	movq	A(,%rdx,8),%rax
	movq	%rax,(%rcx)
	movl	$3640,%eax
	ret 

A.将等式(3.1)从二维扩展到三维,提供数组元素A[i][j][k]的位置的公式。
B.运用你的逆向工程技术,根据汇编代码,确定R、S和T的值。

过程

回顾P178的 3.8.3 嵌套的数组

对于一个声明如下的数组:
T D[R][C];
它的数组元素D[i][j]的内存地址为
    &D[i][j]=x_D+L(C*i+j)	(式3.1)
L是数据类型T以字节为单位的大小。x_D是数组头起始地址,加上L(C*i)得到该行起始地址

示例:5x3的整型数组A,假设x_A,i,j分别在寄存器%rdi,%rsi,%rdx中,可用下面的代码将数组元素A[i][j]复制到寄存器%rax中:
# A in %rdi,i in %rsi,j in %rdx
leaq (%rsi,%rsi,2),%rax	#计算3i
leaq (%rdi,%rax,4),%rax	#计算X_A+12i
movl (%rax,%rdx,4),%eax	#从M[x_A+12i+4j]中读数据
#x_A+12i+4j=x_A+4(3i+j),L=4,C=3

A.将等式(3.1)从二维扩展到三维,提供数组元素A[i][j][k]的位置的公式。

A[R][S][T];
&A[i][j][k]=x_A+L[S*T*i+T*j+k]//S*T*i是算A[i],T*j算A[i][j],+k得到A[i][j][k]

B.汇编代码,确定R、S和T的值。

#long store_ele(long i,long j,long k,long*dest)
#i in %rdi,j in %rsi,k in %rdx, dest in %rcx
store_ele:
	1eaq	(%rsi,%rsi,2),%rax	#%rax=j+2j=3j
	leaq	(%rsi,%rax,4),%rax	#%rax=j+4*(%rax)=13j
	movq	%rdi,%rsi			#%rsi=i,即j=i
	salq 	$6,%rsi				#%rsi=i<<6
	addq	%rsi,%rdi			#%rdi=i+i<<6=i*65
	addq	%rax,%rdi			#%rdi=65i+13j
	addq	%rdi,%rdx 			#%rdx=k+(65i+13j)
	movq	A(,%rdx,8),%rax		#%rax=A+8*[k+(65i+13j)]
	movq	%rax,(%rcx)			#将dest存放的地址修改为A+8*[k+(65i+13j)],
	#即L=8(long为八字节),65=ST,T=13,S=5
	movl	$3640,%eax			#%rax=$3640,即sizeof(A),R*S*T=3640/8=455
	#得到R=455/ST=7
	ret 						

3.65 transpose对矩阵沿着对角线进行转换

题目

下面的代码转置一个MXM矩阵的元素,这里M是一个用#define定义的常数:

void transpose(long A[M] [M]) {
    long i, j;
    for(i = 0; i < M; i++)
        for(j = 0; j < i; j++){
            long t = A[i][j];
            A[i][j] = A[j][i] ;
            A[j][i] = t;
        }
}

当用优化等级-01编译时,GCC为这个函数的内循环产生下面的代码:

#%rcx: t1, %rdx: t2, %rsi: t3, %rax: t4, %rdi: t5
.L6:
	movq		(%rdx), %rcx			
	movq		(%rax), %rsi		
	movq		%rsi, (%rdx)		
	movq		%rcx, (%rax)		
	addq		$8, %rdx			
	addq		$120, %rax				
	cmpq		%rdi, %rax			
	jne			.L6						

我们可以看到GCC把数组索引转换成了指针代码。
A.哪个寄存器保存着指向数组元素A[i][j]的指针?
B.哪个寄存器保存着指向数组元素A[j][i]的指针?
C.M的值是多少?

过程

首先看汇编代码

#%rcx: t1, %rdx: t2, %rsi: t3, %rax: t4, %rdi: t5
#对应内循环 for(j = 0; j < i; j++)
#			 long t = A[i][j];
#            A[i][j] = A[j][i] ;
#            A[j][i] = t;
.L6:
	movq		(%rdx), %rcx			#t1 = *t2,
	movq		(%rax), %rsi			#t3 = *t4,
	movq		%rsi, (%rdx)			#*t2 = t3,
	movq		%rcx, (%rax)			#*t4 = t1,此行和前面完成了对角线元素值的交换
	#*t2的值修改为*t4,*t4的值修改为*t2,即指针t2和t4指向的值进行交换,
	#所以A[i][j]为t2在%rdx中,A[j][i]为t4,在%rax中
	addq		$8, %rdx				#t2 += 8,因为是long,所以L=8,+8即指针t2右移一位(A[i][j] -> A[i][j+1])
	addq		$120, %rax				#t4 += 120,指针t4(A[j][i])移动120/8=15位( A[j][i] -> A[j+1][i]),所以M=15
	cmpq		%rdi, %rax				#比较A[j][i]和边界,即%rdi指向A[M][M]
	jne			.L6						#if A[j][i] != A[M][M]

3.66 sum_co1确定NR和Nc的定义

题目

考虑下面的源代码,这里NR和NC是用#define声明的宏表达式,计算用参数n表示的矩阵A的维度。这段代码计算矩阵的第j列的元素之和

long sum_co1(long n, long A[NR(n)][NC(n)], long j) {
	long i;
	long result = 0;
	for (i = 0; i < NR(n); i++)
		result += A[i][j];
	return result;
}

编译这个程序,GCC产生下面的汇编代码:

long sum_col(long n, long A[NR(n)][NC(n)], long j)
#n in %rdi, A in %rsi, j in %rdx
sum_col:
	leaq	1(,%rdi,4), %r8			
	leaq	(%rdi,%rdi,2), %rax		
	movq	%rax, %rdi				
	testq	%rax, %rax				
	jle		.L4					
	salq	$3, %r8				    
	leaq	(%rsi,%rdx,8), %rcx		
	movl	$0, %eax			
	movl	$0, %edx			
.L3:
	addq	(%rcx), %rax		
	addq	$1, %rdx			
	addq	%r8, %rcx				
	cmpq	%rdi, %rdx			
	jne		.L3				
	rep; ret
.L4:
	movl	$0, %eax			
ret

运用你的逆向工程技术,确定NR和Nc的定义。

过程

根据汇编代码:

long sum_col(long n, long A[NR(n)][NC(n)], long j)
#n in %rdi, A in %rsi, j in %rdx
sum_col:
	leaq	1(,%rdi,4), %r8			 #%r8 = 1 + 4 * n
	leaq	(%rdi,%rdi,2), %rax		 #%rax = 3 * n
	movq	%rax, %rdi				 #%rdi = %rax = 3 * n
	testq	%rax, %rax				 #检验 %rax
	jle		.L4					     #if %rax <= 0, goto L4
	salq	$3, %r8				     #%r8 = %r8 << 3 ,即8 *(4n+1)
	leaq	(%rsi,%rdx,8), %rcx		 #%rcx = %rsi + 8 * %rdx = A + 8 * j,所以开始时%rcx = A[0][j]的地址
	movl	$0, %eax			     #%rax = 0 ,因为result要返回,所以在%rax中,即result = 0
	movl	$0, %edx				 #%rdx = 0,即i = 0
.L3:
	addq	(%rcx), %rax			 #%rax = %rax + *rcx,即result += A[i][j]
	addq	$1, %rdx				 #%rdx = %rdx + 1,即i++
	addq	%r8, %rcx				 #%rcx = %rcx + %r8,此时%rcx是A[i+1][j]地址,
	#所以位移量8*%r8=8*(4n+1)==>L=8,C=4n+1,每行有4n+1个,NR=4n+1
	cmpq	%rdi, %rdx				 #比较 %rdx - %rdi,比较i和3n,C代码中的判断 i < NR(n),所以NR=3n
	jne		.L3					     #if %rdx - %rdi != 0,相等就结束循环
	rep; ret
.L4:
	movl	$0, %eax				 #%	rax = 0
ret

3.67 函数eval,调用process

题目

这个作业要查看GCC为参数和返回值中有结构的函数产生的代码,由此可以看到这些语言特性通常是如何实现的。
下面的C代码中有一个函数process,它用结构作为参数和返回值,还有一个函数eval,它调用process:

typedef struct {
	long a[2];
	long* p;
}strA;

typedef struct {
    long u[2];
    long q;
}strB;

strB process(strA s) {
	strB r;
	r.u[0] = s.a[1];
	r.u[1] = s.a[0];
	r.q = *s.P;
    return r;
}

long eval (1ong x, long y, long z){
	strA s;
	s.a[0] = x;
	s.a[1] = y;
	s.p = &z;
	strB r = process(s);
	return r.u[0] + r.u[1] + r.q;
)

GCC为这两个函数产生下面的代码:

strB process(strA s)
process:
	movq	%rdi, %rax		
	movq	24(%rsp), %rdx	
	movq	(%rdx), %rdx	
	movq	16(%rsp), %rcx	
	movq	%rcx, (%rdi)
	movq	8(%rsp), %rcx	
	movq	%rcx, 8(%rdi)	
	movg	%rdx, 16(%rdi)	
	ret
	
long eval(long x,long y, long z)
#x in %rdi, y in %rsi, z in %rdx
eval:
	subq	$104, %rsp		
	movq	%rdx, 24(%rsp)		
	leaq	24(%rsp), %rax		
	movq	%rdi, (%rsp)	
	movq	%rsi, 8(%rsp)	
	movq	%rax, 16(%rsp)		
	leaq	64(%rsp), %rdi		
	call	process
	movq	72(%rsp), %rax		
	addq	64(%rsp), %rax		
	addq	80(%rsp), %rax		
	addq	$104, %rsp			
	ret

A.从eval函数的第2行我们可以看到,它在栈上分配了104个字节。画出eval的栈帧,给出它在调用process前存储在栈上的值。
B.eva1 调用process时传递了什么值?
C.process的代码是如何访问结构参数s的元素的?
D.process的代码是如何设置结果结构r的字段的?
E.完成eval的栈帧图,给出在从process返回后eval是如何访问结构r的元素的。
F.就如何传递作为函数参数的结构以及如何返回作为函数结果的结构值,你可以看出什么通用的原则?

过程

首先看汇编代码

long eval(long x,long y, long z)
#x in %rdi, y in %rsi, z in %rdx
eval:
	subq	$104, %rsp		#栈顶指针下移104(13*8)字节分配空间,13个八字节
	movq	%rdx, 24(%rsp)	#*(rsp + 24) = z,把z的值保存在偏移量为24的位置,即z存入栈指针开始的第4个八字节(24/8+1)
	leaq	24(%rsp), %rax	#%rax=rsp+24=>&z,%rax保存z的指针,∵C代码s.p = &z;所以栈指针开始的第4个八字节存的s.p
	movq	%rdi, (%rsp)	#*rsp = x,x保存在偏移量为0的位置,即栈指针开始的第1个八字节,即s.a[0] = x
	movq	%rsi, 8(%rsp)	#*(rsp + 8) = y,y保存在栈指针开始的第2个八字节,即s.a[1] = y
	movq	%rax, 16(%rsp)	#*(rsp + 16) = z,栈指针开始的第3个八字节,即s.p = &z
	leaq	64(%rsp), %rdi	#栈指针开始的第9个八字节的开始地址,当做参数传递给后边的函数
	call	process			#有隐藏操作,分配八字节栈空间,存入返回地址,即下一行代码地址	
	movq	72(%rsp), %rax	#%rax=*(rsp + 72),这三行实现加法,即 %rax= r.u[1]
	addq	64(%rsp), %rax	#%rax=%rax+*(rsp +64),即 %rax+=r.u[0]
	addq	80(%rsp), %rax	#%rax=%rax+*(rsp +80),即 %rax+=r.q
	addq	$104, %rsp		#%rsp=%rsp+104,回收栈空间
	ret
	
strB process(strA s)
process:#因为有有隐藏操作,此时rsp又-8
	movq	%rdi, %rax		#把参数保存到%rax,第一个参数作为返回值,即要返回的结构体的开始地址(上面栈指针开始的第9个八字节的开始地址)
	movq	24(%rsp), %rdx	#%rdx=*(rsp + 24),即%rdx=s.p
	movq	(%rdx), %rdx	#%rdx存放的s.p寻址得到存放的内容z,再存入%rdx,即%rdx=*(s.p)=z
	movq	16(%rsp), %rcx	#栈指针开始的第3个八字节的内容(原eval中的第2个,即s.a[1]),%rcx=*(rsp + 16),即 s.a[1]
	movq	%rcx, (%rdi)	#将s.a[1],存入返回结构体的第1个八字节,r.u[0]=s.a[1]
	movq	8(%rsp), %rcx	#%rcx = *(rsp + 8),即s.a[0]
	movq	%rcx, 8(%rdi)	#将s.a[0],存入返回结构体的第2个八字节,r.u[1]=s.a[0]
	movg	%rdx, 16(%rdi)	#r.q=*(s.p),存入返回结构体的第3个八字节
	#栈指针开始的第1个八字节,这里并没有使用,因为存的是调用后的返回地址
	ret

接下来回答问题

A.从eval函数的第2行我们可以看到,它在栈上分配了104个字节。画出eval的栈帧,给出它在调用process前存储在栈上的值。

104  +------------------+
     |                  |
     |                  |
     |                  |
     |                  |
     |                  |
     |                  |
     +------------------+
     |                  |(第九个八字节)
 64  +------------------+ <-- %rdi
     |                  |
     |                  |
     |                  |
     |                  |
     |                  |
     |                  |
 32  +------------------+
     |         z        |
 24  +------------------+
     |        &z        |					 (即s.p = &z)
 16  +------------------+	
     |         y        |					 (即s.a[1] = y)
  8  +------------------+
     |         x        |(第一个八字节)		(即s.a[0] = x)
  0  +------------------+ <-- %rsp

B.eva1 调用process时传递了什么值?

传递了一个相对于%rsp偏移量为64的指针,即栈指针开始的第9个八字节的开始地址。

C.process的代码是如何访问结构参数s的元素的?

因为结构参数s存在栈空间里,所以用%rsp+偏移量来访问的。

D.process的代码是如何设置结果结构r的字段的?

r的空间是分配在栈空间里,所以也是%rsp+偏移量来设置的。

E.完成eval的栈帧图,给出在从process返回后eval是如何访问结构r的元素的。

104  +------------------+
     |                  |
     |                  |
     |                  |
     |                  |
     |                  |
     |                  |
 88  +------------------+----
     |        z         |	-								(即r.q)
 80  +------------------+	-
     |        x         |	--->(64到88)是返回结构体		   (即r.u[1])
 72  +------------------+	-								
     |        y         |	-								(即r.u[0])
 64  +------------------+ <-- %rdi(eval pass in)
     |                  |  \
     |                  |   -- %rax(process pass out)
     |                  |
     |                  |
     |                  |
     |                  |
 32  +------------------+
     |         z        |
 24  +------------------+----
     |        &z        |   -
 16  +------------------+	-
     |         y        |	---->(0到24)是传入结构体
  8  +------------------+	-
     |         x        |	-
  0  +------------------+ <-- %rsp in eval
     |                  |
 -8  +------------------+ <-- %rsp in process

F.就如何传递作为函数参数的结构以及如何返回作为函数结果的结构值,你可以看出什么通用的原则?

结构体作为参数传入和返回时,都是以指针来传递。

3.68 set_val求A,B值

题目

在下面的代码中,A和B是用#define定义的常数:

typedef struct{
	int x[A][B];/*Unknown constants A and B*/
	long y;
}str1;
typedef struct{
	char array[B];
    int t;
    short s[A];
	long u;
}str2;
void setVal(strl *p,str2 *q){
	long v1=q->t;
    long v2=q->u;
    p->y=v1+v2;
}

GCC为setVal产生下面的代码:

#void setVal(strl*p,str2*g)
#p in %rdi,g in %rsi 
setVal:
	movslq 	8(%rsi),%rax
	addq	32(%rsi),%rax 
	movq	%rax,184(%rdi)
ret

A和B的值是多少?(答案是唯一的。)

过程

#void setVal(strl*p,str2*q)
#p in %rdi,q in %rsi  
setVal:
	movslq 	8(%rsi),%rax	#%rax=*(8 + q),访问q->t,所以char array[B]占据8字节,因为需要考虑对齐原则,所以先得出 B <= 8
	addq	32(%rsi),%rax 	#%rax=%rax+*(32 + q),计算v1+v2,通过+32访问q->u,32减去int占据的4得到short s[A]占据的,所以4 + A * 2 <= 32 - 8 <= 24得到A<=10
	movq	%rax,184(%rdi)	#*(184 + p)=%rax,+184访问p->y,由于对齐原则是保证8的倍数,所以176 < 4*A*B <= 184,得44 < A*B <=46
	#所以A*B = 45 或者A*B = 46,结合A, B各自的范围,只可能为A = 9, B = 5.
ret

3.69 逆向推断CNT和结构a_struct的完整声明

题目

你负责维护一个大型的C程序,遇到下面的代码:

typedef struct {
    int first;
    a_struct a[CNT] ;
    int last;
} b_struct;

void test(long i, b_struct *bp)
{
    int n = bp->first + bp->last;
    a_struct *ap = &bp->a[i];
    ap->x[ap->idx] = n;
}

编译时常数CNT和结构a_struct的声明是在一个你没有访问权限的文件中。幸好,你有代码的‘.o’版本,可以用OBJDUMP程序来反汇编这些文件,得到下面的反汇编代码:

void test(long i, b_struct *bp)
i in %rdi, bp in %rsi:
000000000000000 <test>:
	0:	8b 8e 20 01 00 00	mov	0x120(%rsi),%ecx   		
	6:	03 0e            	add	(%rsi),%ecx		   	
	8:	48 8d 04 bf      	lea	(%rdi,%rdi,4), %rax	 
	c:	48 8d 04 c6      	lea	(%rsi,%rax,8), %rax   
	10:	48 8b 50 08 		mov	0x8(%rax),%rdx       	
	14:	48 63 c9         	movslq %ecx,%rcx		
	17:	48 89 4c d0 10   	mov	%rcx,0x10(%rax,%rdx,8)	
	1c:	c3               	retq

运用你的逆向工程技术,推断出下列内容:
A.CNT的值。
B.结构a_struct的完整声明。假设这个结构中只有字段idx和x,并且这两个字段保存的都是有符号值。

过程

#void test(long i, b_struct *bp)
#i in %rdi, bp in %rsi:
000000000000000 <test>:
	0:	8b 8e 20 01 00 00	mov	0x120(%rsi),%ecx   		#%rcx=*(bp+288),访问bp->last(因为first和a[CNT]都不会+288),由于对齐原则是保证8的倍数,所以8+CNT*sizeof(a_struct)=288
	6:	03 0e            	add	(%rsi),%ecx		   		#%rcx+=*bp=>*(bp+288)+*bp,即bp->first+bp->last,可知%rcx存的n
	8:	48 8d 04 bf      	lea	(%rdi,%rdi,4), %rax	 	#%rax=5*i
	c:	48 8d 04 c6      	lea	(%rsi,%rax,8), %rax   	#%rax=bp+8*%rax=bp+40*i
  
	10:	48 8b 50 08 		mov	0x8(%rax),%rdx       	#%rdx=*(%rax+8),即bp+40*i+8,
	# ap = &bp->a[i] = bp+8+i*40, +8意味着从bp开始的第1个八字节里面只有int(first占8),且a_struct大小必为8字节或更大,若为4字节,就不是+8而是+4了(不然4+4可以直接跟8对齐不需要将4填补4字节来对齐)
	# 因为是i*40,所以a_struct大小为40字节,因为first占8个字节,所以cnt=(288-8)/40=7
	# 此句很明显取出了一个数,再结合倒数第二条指令mov %rcx, 0x10(%rax,%rdx,8),所以%rdx为ap->idx,
	#而且在结构体a_struct中,第一个成员为整数类型的idx
	
	14:	48 63 c9         	movslq %ecx,%rcx			#%rcx = %ecx (带符号拓展)
	17:	48 89 4c d0 10   	mov	%rcx,0x10(%rax,%rdx,8)	#*(%rax+8*%rdx+16)=%rcx,即ap->x[ap->idx]=n,
	# 先看0x10(%rax,)部分,是bp+16+i*40,比ap多了8字节,这里是a_struct数组成员的开始地址,也说明了idx大小为8字节
 	# 再看(,%rdx,8)部分,是idx*8,所以说明了a_struct数组成员的大小为8字节
 	# 合起来看就是bp+8+i*40+8 +idx*8,第二个+8跳过了a_struct的整数成员idx
 	
	# a_struct大小为40字节,第一个成员idx为long,8字节,还剩32字节
  	# 第二个成员是long型数组,按照剩余字节,数组大小为4	
	1c:	c3               	retq
typedef struct {
	long idx;
	long x[4];
} a_struct;

3.70 void proc(union ele up)

题目

考虑下面的联合声明:

union ele{
	struct{
		long *p;
		long y;
	}e1;
	struct{
		long x;
        union ele *next;
	}e2;
};

这个声明说明联合中可以嵌套结构。
下面的函数(省略了一些表达式)对一个链表进行操作,链表是以上述联合作为元素的:

void proc(union ele *up){
	up->_____=*( _____ )- _____;
}

A.下列字段的偏移量是多少(以字节为单位):

字段 偏移量
e1.p
e1.y
e2.x
e2.next

B.这个结构总共需要多少个字节?
C.编译器为proc产生下面的汇编代码:

#void proc(union ele up)
#up in %rdi proc:
proc:
    movq    8(%rdi), %rax  #
    movq    (%rax), %rdx   #
    movq    (%rdx), %rdx   #
    subq    8(%rax), %rdx  #
    movq    %rdx, (%rdi)   #
    ret

在这些信息的基础上,填写proc代码中缺失的表达式。提示:有些联合引用的解释可以有歧义。
当你清楚引用指引到哪里的时候,就能够澄清这些歧义。只有一个答案,不需要进行强制类型转换,且不违反任何类型限制。

过程

A.因为访问union中的元素不需要加偏移量,所以e1.p和e2.x作为e1和e2的第一个元素偏移量都是0

字段 偏移量
e1.p 0
e1.y 8
e2.x 0
e2.next 8

B.这个结构总共需要多少个字节?

union需要的字节等于它最大字段的大小,8+8=16

C.写proc

首先为汇编代码加注释

#void proc(union ele up)
#up in %rdi proc:
proc:
    movq    8(%rdi), %rax  #偏移量为8,存的是up->e1.y或者是up->e2.next
    movq    (%rax), %rdx   #用作内存引用(P121),取出该地址中的值,所以上面是up->e2.next,
    #取出*(up->e2.next)的偏移量为0的内容,也有两种情况: *(up->e2.next).e1.p或*(up->e2.next).e2.x)
    
    movq    (%rdx), %rdx   #用作内存引用,所以上面是*(up->e2.next).e1.p,取出*( *(up->e2.next).e1.p )的内容,为long型
    subq    8(%rax), %rdx  #取出*(up->e2.next)的偏移量为8的内容,因为要作为减数,所以减数是*(up->e2.next).e1.y
    movq    %rdx, (%rdi)   #将减法之差存入,up->e2.x
    ret
void proc (union ele *up) {
    up->e2.x = *(*(up->e2.next).e1.p) - up->e2.next.e1.y;
}

3.71 写一个函数good_echo

题目

写一个函数good_echo,它从标准输入读取一行,再把它写到标准输出。你的实现应该对任意长度的输入行都能工作。可以使用库函数fgets,但是你必须确保即使当输入行要求比你已经为缓冲区分配的更多的空间时,你的函数也能正确地工作。你的代码还应该检查错误条件,要在遇到错误条件时返回。参考标准I/O函数的定义文档[45,61]。

过程

这道题主要需要了解fgets函数(char * fgets ( char * str, int num, FILE * stream );)。下面将fgets函数的api文档进行翻译。

Get string from stream

从流中读取字符,并将其作为一个C语言字符串存储到str中,直到读完(num-1)个字符或达到换行符或文件结束,以先发生者为准
换行符使fgets停止读取,但它被函数认为是一个有效的字符,并包括在拷贝到str的字符串中。
一个结束性的空字符会自动附加在复制到str的字符之后。
请注意,fgets与gets完全不同:fgets不仅接受一个流参数,而且允许指定str的最大尺寸,并在字符串中包括任何结束的换行字符。

Return Value

成功时,该函数返回str。
如果在试图读取一个字符时遇到了文件结束,则设置eof指示器(ffe)。如果在可以读取任何字符之前发生这种情况,返回的指针是一个空指针(而str的内容保持不变)。
如果发生读取错误,错误指示器(ferror)被设置,同时返回一个空指针(但str所指向的内容可能已经改变)。

#include <stdio.h>
#include <assert.h>
#define BUF_SIZE 2

void good_echo() {
	char buf[BUF_SIZE];
	while(1) {
		char* p = fgets(buf, BUF_SIZE, stdin);
		if (p == NULL) {//修改为if ( (p == NULL) & (ferror(stdin) != 0) )
			break;
		}
		printf("%s", p);
	}
	return;
}

int main() {
	good_echo();
	return 0;
}

1.根据翻译得知,使用fgets函数便可以保证“当输入字符超过缓冲区空间大小时,也能正常工作”。
2.关于“你的代码还应该检查错误条件,在遇到错误条件时返回”这点,其实判断条件if (p == NULL)太笼统了,可以通过ferror函数int ferror ( FILE * stream );)来判断(stdin的类型是FILE *),当读取出错时,调用ferror函数返回非0值,上述代码应写成if ( (p == NULL) & (ferror(stdin) != 0) )

3.72 aframe

题目

图3-54a给出了一个函数的代码,该函数类似于函数vfunct(图3-43a)。我们用vfunct来说明过帧指针在管理变长栈帧中的使用情况。这里的新函数aframe 调用库函数alloca为局部数组p分配空间。alloca类似于更常用的函数malloc,区别在于它在运行时栈上分配空间。当正在执行的过程返回时,该空间会自动释放。
图3-54b给出了部分的汇编代码,建立帧指针,为局部变量i和p分配空间。非常类似于vframe对应的代码。在此使用与练习题3.49中同样的表示法:栈指针在第4行设置为值s1,在第7行设置为值s2。数组p的起始地址在第9行被设置为值p。s₂和p之间可能有额外的空间e2,数组p结尾和s1之间可能有额外的空间e1。
A.用数学语言解释计算s₂的逻辑。
B.用数学语言解释计算p的逻辑。
C.确定使e1的值最小和最大的n和s1的值。
D.这段代码为s2和p的值保证了怎样的对齐属性?

#include <alloca.h>
long aframe(long n, long idx, long *q) f
	long i;
	long **p = alloca(n * sizeof (long *)) ;
	p[O] = &i;
	for(i = 1; i < n; i++)
		p[i] = q;
	return *p[idx];
}
#long aframe(long n, long idx, long *q)
#n in %rdi, idx in %rsi, q in %rdx
aframe : 
	pushq	%rbp				#
	movq	%rsp, %rbp			#	
	subq	$16, %rsp        	Allocate space for i (%rsp = s1)
	leaq	30(,%rdi,8), %rax	#
	andq	$-16, %rax       	#
	subq	%rax, %rsp       	Allocate space for array p (%rsp = s2)
	leaq	15(%rsp), %r8    	#
	andq	$-16, %r8        	set %r8 to &p[0]
	...

过程

注意c语句long **p = alloca(n * sizeof(long*));,p的类型为long **即long指针的指针,可以这么理解,分配long型数组时,返回long *指针;当分配long *型数组时,返回long **指针。

根据P202对变成栈帧的介绍,添加注释

#long aframe(long n, long idx, long *q)
#n in %rdi, idx in %rsi, q in %rdx
aframe : 
	pushq	%rbp				#
	movq	%rsp, %rbp			#	
	subq	$16, %rsp        	Allocate space for i (%rsp = s1)
	leaq	30(,%rdi,8), %rax	#%rax=30+8n
	andq	$-16, %rax       	#%rax=%rax& (-1), 设置末4位为0
	#这里的-16的十六进制表示为0xfffffff0,之所以用& 就是为了求16的整数倍
	subq	%rax, %rsp       	Allocate space for array p (%rsp = s2)
	leaq	15(%rsp), %r8    	#%r8=%rsp+15
	andq	$-16, %r8        	set %r8 to &p[0]
	...

andq $-16, %rax分为两种情况:(and -16解释为向下取整到16的倍数)
a.当为偶数时,分成8n和30两部分,8n and -16得8n,30 and -16得16.
b.当为奇数时,分成8(n-1)和38两部分,8(n-1) and -16得8(n-1),38 and -16得32.
leaq 15(%rsp), %r8 加上偏置15(2^4-1),第9行 and -16,执行完这两行,就相当于向上取整到16的倍数。注意在练习题3.49中,andq $-16, %r8这句是通过两句汇编来实现的(先右移再左移,而本题是直接and -16)。

画栈帧图

----------     		 <-- 	%rbp  0
   i
-------8
(未被使用的)
---------- s1    	 <-- 	%rbp-16
    e1
----------

    p
    
---------- p
    e2
---------- s2

A.用数学语言解释计算s₂的逻辑。

\[\begin{aligned} s_1 &=\%\text{rsp} - 16\\ s_2 &=s_1- \left((30 + 8 \times n )- (30 + 8 \times n )\bmod 16 \right) \end{aligned} \]

B.用数学语言解释计算p的逻辑。

\[p= (s_2 + 15) - \left((s_2 + 15)\bmod 16 \right) \]

C.确定使e1的值最小和最大的n和s1的值。

s2可以写为s2 = s1 - (0xfffffff0 & (8n + 30)) 根据这个公式

  • 当n是偶数的时候,我们可以把式子简化为s2 = s1 - (8 * n + 16)

  • 当n是奇数的时候,我们可以把式子简化为 s2 = s1-[8(n-1)+32)]=s1 - (8 * n + 24)

大方向分为,当s2为16的倍数(这种情况p数组就直接从s2开始分配),和s2不为16的倍数(这种情况p数组还需要向地址增加方向滑动1-15个字节)。

1.因为e1和e2是用来滑动的,所以当e2为0,即s2为16的倍数时,当e1就会最大。再看当n为奇数时,分配数组空间为8 n + 24,多出来24字节空间作为e1。e1最大为24,此时s2为16的倍数,且n为奇数。

2.当s2不为16的倍数时,p数组空间需要滑动来对齐16,当s2 mod 16=1时,向地址增加方向滑动15个字节,此时达到最大滑动距离了,即e2=15。而e1=可滑动空间-e2,当n为偶数时,滑动空间为16字节,则e1=可滑动空间-e2=16-15=1。e1最小为1,此时s2 mod 16=1,且n为偶数。

D.这段代码为s2和p的值保证了怎样的对齐属性?

s2 确保能够容纳足够的p, p能够保证自身16对齐,即保证了数组的起始位置都是16的倍数。

3.73 条件分支指令修改find_range汇编代码

题目

用汇编代码写出匹配图3-51中函数find_range行为的函数。你的代码必须只包含一个浮点比较指令,并用条件分支指令来生成正确的结果。在2^32种可能的参数值上测试你的代码。网络旁注ASM:EASM描述了如何在C程序中嵌入汇编代码。

图3-51 (a)
typedef enum { NEG, ZERO, POS, OTHER } range_t;

range_t find_range(float x){
    range_t result;
    if (x < 0){
        result = NEG;
    }else if (x == 0){
        result = ZERO;
    }else if (x > 0){
        result = POS;
    }else{
        result = OTHER;
    }
    return result;
}
图3-51 (b)
#range_t find_range(float x)
#x in %xmm0
find_range: 
	vxorps	%xmm1,%xmm1,%xmm1	#Set % xmm1=0
	vucomiss	%xmm0,%xmm1		#Compare 0:x 比较%xmm1-%xmm0,即0-x
	ja	.L5						#If>, goto neg 
	vucomiss	%xmm1,% xmm0 	#Compare x:0,比较x-0
	jp	.L8						#If NaN, goto posornan 
	movl	$1,%eax				#result=ZERO 
	je	.L3						#If=, goto done
.L8: 							#posornan: 
	vucomiss	.LC0(%rip),%xmm0 #Compare x:0
	setbe %al					#Set result=NaN?1:0
	movzbl	%al,%eax			#Zero-extend 
	add1	$2,%eax				#result+=2(POS for>0, OTHER for NaN)
	ret							#Return
.L5: 							#neg:
	movl	$0,%eax				#result=NEG
.L3: 							#done:
	rep;ret						#Return

过程

修改汇编代码再进行比较

vxorps %xmm1, %xmm1, %xmm1	#Set % xmm1=0
vucomiss %xmm0, %xmm1		#Compare 0:x 比较%xmm1-%xmm0,即0-x
ja .LA						#0>x
je .LB						#0=x
jb .LC						#0<x
jp .LD						#NaN
.LA:						#
    movl $0, %eax			#
    jmp .LE					#
.LB:						#
    movl $1, %eax			#
    jmp .LE					#
.LC:						#
    movl $2, %eax			#
    jmp .LE					#
.LD:						#
    movl $3, %eax			#
.LE:						#

比较

#include <stdio.h>
#include <limits.h>

typedef enum { NEG, ZERO, POS, OTHER } range_t;

range_t find_range_origin(float x){
    range_t result;
    if (x < 0){
        result = NEG;
    }else if (x == 0){
        result = ZERO;
    }else if (x > 0){
        result = POS;
    }else{
        result = OTHER;
    }
    return result;
}

range_t find_range(float x){
    //不能使用ret
    asm(
        "vxorps %xmm1, %xmm1, %xmm1\n\t"	
        "vucomiss %xmm0, %xmm1\n\t"
        "ja .LA\n\t"
        "je .LB\n\t"
        "jb .LC\n\t"
        "jp .LD\n\t"
        ".LA:\n\t"
            "movl $0, %eax\n\t"
            "jmp .LE\n\t"
        ".LB:\n\t"
            "movl $1, %eax\n\t"
            "jmp .LE\n\t"
        ".LC:\n\t"
            "movl $2, %eax\n\t"
            "jmp .LE\n\t"
        ".LD:\n\t"
            "movl $3, %eax\n\t"
        ".LE:\n\t"
    );
}

void test(){
    bool flag = true;
    int cnt = 0;
    for (short i = SHRT_MIN; i < SHRT_MAX; i++){
        if (find_range_origin(i) != find_range(i)){
            printf("Wrong result for %d!\n", i);
            flag = false;
            break;
        }
    }

    if (flag){
        printf("Passed the test!\n");
    }
}

int main(){
    test();
    return 0;
}

3.74 条件传送指令修改find_range汇编代码

题目

用汇编代码写出匹配图 3-51 中函数find_range行为的函数。你的代码必须只包含一个浮点比较 指令,并用条件传送指令来生成正确的结果。你可能会想要使用指令 cmovp(如果设置了偶校验位 传送)。 在 2^32 种可能的参数值上测试你的代码。网络旁注 ASM:EASM 描述了如何在 C 程序中嵌入汇编代码。

过程

P147条件传送实现分支

find_range:
    vxorps %xmm1, %xmm1, %xmm1	#x设置为0
    movq $0, %r8				#
    movq $1, %r9				#
    movq $2, %r10				#
    movq $3, %rax				#默认为NaN
    vucomiss %xmm1, %xmm0		#比较x:0,
    cmovb %r8, %rax				#x<0,设置为0
    cmove %r9, %rax				#x=0
    cmova %r10, %rax			#x>0
    ret

3.75 复数

题目

ISO C99 包括了支持复数的扩展。任何浮点类型都可以用关键字complex修饰。这里有一些使用 复数数据的示例函数,调用了一些关联的库函数:

#include <complex.h>
double c_imag(double complex x) {
    return cimag(x);
}
double c_real(double complex x) {
    return creal(x) ;
}
double complex c_sub(double complex x, double complex y) {
    return x - y;
}

编译时,GCC 为这些函数产生如下代码:

double c_imag(double complex x)
c_imag:
	movapd	%xmm1, %xmm0		# 
	ret                 		#
	
double c_real (double complex x)
c_real:
	rep; ret             		#
	
double complex c_sub(double complex x, double complex y)
c_sub:
	subsd	%xmm2, %xmm0		#
	subsd	%xmm3, %xmm1		#
	ret										

根据这些例子,回答下列问题:

A.如何向函数传递复数参数?

B. 如何从函数返回复数值?

过程

复数 = 实数 + 虚数 传参的时候,有这样的规律 (复数1, 复数2, 复数3...)

对应的浮点寄存器就会是: %xmm0, %xmm1, %xmm2,...

第n个参数 real img
1 %xmm0 %xmm1
2 %xmm2 %xmm3
3 %xmm4 %xmm5
n %xmm(2n-2) %xmm(2n-1)

先注释汇编代码

double c_imag(double complex x)
c_imag:
	movapd	%xmm1, %xmm0		#r0 = r1
	ret                 		#return r0
	
double c_real (double complex x)
c_real:
	rep; ret             		#return r0
	
double complex c_sub(double complex x, double complex y)
c_sub:
	subsd	%xmm2, %xmm0		#t0 -= t2实部相减
	subsd	%xmm3, %xmm1		#t1 -= t3虚部相减
	ret																

A.如何向函数传递复数参数?

%xmm0存储实部,%xmm1存储虚部。

B. 如何从函数返回复数值?

返回%xmm0,%xmm1。

posted @ 2023-02-06 10:15  付玬熙  阅读(936)  评论(0编辑  收藏  举报