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位时,它们可以重写为
这里xh,xl,yh,yl都是64位值。类似地,128位的乘积可以写成
这里ph和pl是64位值。请解释这段代码是如何用xh,xl,yh,yl来计算ph和pl的。
过程
需要看P133的3.5.5 特殊的算术操作,介绍了汇编代码中imulq
和mulq
等指令,和题目中的函数。
下标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当做无符号数。因为符号位现在在高位寄存器那儿呢,所以高位寄存器当做补码数了;而低位寄存器的每一位的权值现在都是正的了,所以低位寄存器要当做无符号数。
首先看公式,
然后看汇编代码
//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'和y'两个位级向量,固定将x'看作补码数,而将y'分别看作补码数和无符号数,那么x与y的两种乘积的截断的位级表示是一样的。接下来用个小例子来证明该结论。(注意代码是将乘积的截断的位级表示看作补码数的)
假设整数类型为3位,x'和y'分别为
111
和111
,x的值为-1,而y的值分别为-1,7.
首先看-1 * -1 = 1,那么位级表示为001
再看-1 * 7 = -7,那么位级表示为1001
,截断后为001
证毕。
考虑下第9行是否会溢出,
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-expr
和else-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
中对空指针的计算。