《深入理解计算机系统》第三版 第三章家庭作业答案
簡述
相信大部分人在做這些題的時候,因為書中沒有給答案,而去網上找參考答案,比如那些高閱讀量的博客和git。當然,我也是這樣,但他們的答案中還是有好多錯誤,比如3.59他們幾乎都沒講清楚提示中的公式怎么來的,3.60中對移位操作中對%cl的讀取,等等。。希望讀者們在閱讀這些文章時,要帶著自己的思想和疑問去理解,而不是一味地覺得答案就肯定是對的,當然,本文有任何錯誤,也歡迎各位指出。
3.58
long decode2(long x,long y,long z) {y = y - z;x = x * y;y <<= 63;y >>= 63;return y ^ x; }y先左移63位,再右移63位,如果之前y是奇數,那么y的二進制全是1;y是偶數,那么y的二進制全是0.
3.59
首先講解一下,提示里的公式x=264?xh+xlx=2^{64}*x_h+x_lx=264?xh?+xl?,之所以可以這么寫是因為符號拓展,以4位二進制int為例:
1111的補碼數,為-1.將其進行符號拓展后為1111 1111,其值也為-1,但這里可以將1111 1111寫為高位1111的補碼數 * 242^424 + 低位1111的無符號數:
即-1 * 242^424 + 15 = -1.
原理:%rdx和%rax的二進制連起來表示這個數,既然連起來了,符號位就跑到了%rdx的最高位了,除符號位權值為負外,其余位的權值均為正。所以,高位寄存器%rdx當做補碼數,低位寄存器%rax當做無符號數。因為符號位現在在高位寄存器那兒呢,所以高位寄存器當做補碼數了;而低位寄存器的每一位的權值現在都是正的了,所以低位寄存器要當做無符號數。
所以xlx_lxl?為T2U(x)T2U(x)T2U(x)即x的二進制表示作為無符號數。xlx_lxl?與xxx有相同的位級表示。
xhx_hxh?,當原數符號位為1,64位二進制位上全為1,其值為-1;當原數符號位為0時,64位二進制位上全為0,其值為0。
再講解一下本文用到的數學公式:有x=264?xh+xlx=2^{64}*x_h+x_lx=264?xh?+xl?和y=264?yh+yly=2^{64}*y_h+y_ly=264?yh?+yl?,那么有:
x?y=(264?xh+xl)?(264?yh+yl)x*y=(2^{64}*x_h+x_l)*(2^{64}*y_h+y_l)x?y=(264?xh?+xl?)?(264?yh?+yl?)
=xhyh2128+(xhyl+xlyh)264+xlyl=x_hy_h2^{128}+(x_hy_l+x_ly_h)2^{64}+x_ly_l=xh?yh?2128+(xh?yl?+xl?yh?)264+xl?yl?
但這個公式其實并不陌生,它與2.3.5補碼乘法(P67) 里面的公式2.18有異曲同工之妙,另外理解本題需要閱讀此節。
第一項xhyh2128x_hy_h2^{128}xh?yh?2128肯定溢出,雙寄存器都裝不下,截斷后全為0,忽略。
關于第二項,(xhyl+xlyh)(x_hy_l+x_ly_h)(xh?yl?+xl?yh?)這個數值是需要放在高位寄存器中的(因為這一項乘以的數為2642^{64}264),假設xhylx_hy_lxh?yl?分別是-1和UMAX,僅僅是它倆的乘積都會使得高位寄存器溢出(考慮補碼數和無符號數的表示范圍就能想到),如果溢出,放入高位寄存器時會自行截斷。
第三項xlylx_ly_lxl?yl?,直接使用雙寄存器來保存結果。
下面開始講解匯編代碼:
第一個參數*dest在%rdi中,第二個參數x在%rsi中,第三個參數y在%rdx中。
重點講一下6-8行,發現這里代碼計算的是(xhy+xyh)(x_hy+xy_h)(xh?y+xyh?),而數學公式里面要求是(xhyl+xlyh)(x_hy_l+x_ly_h)(xh?yl?+xl?yh?),之所以匯編要如此計算,是利用了相同的位級向量,無論用無符號數乘法還是補碼乘法,其結果的截斷的位級表示肯定是一樣的。
但這里有點不一樣,給定x?\vec xx和y?\vec yy?兩個位級向量,固定將x?\vec xx看作補碼數,而將y?\vec yy?分別看作補碼數和無符號數,那么x與y的兩種乘積的截斷的位級表示是一樣的。接下來用個小例子來證明該結論。(注意代碼是將乘積的截斷的位級表示看作補碼數的)
假設整數類型為3位,x?\vec xx和y?\vec yy?分別為111和111,x的值為-1,而y的值分別為-1,7.
首先看-1 * -1 = 1,那么位級表示為001
再看-1 * 7 = -7,那么位級表示為1001,截斷后為001
證畢。
考慮下第9行是否會溢出,無符號數最大為264?12^{64}-1264?1,所以兩個無符號數的乘積最大為(264?1)2(2^{64}-1)^2(264?1)2等于2128+1?2652^{128}+1-2^{65}2128+1?265.而128位的補碼數的最大范圍為2127?12^{127}-12127?1.
而(2128+1?265)?(2127?1)(2^{128}+1-2^{65})-(2^{127}-1)(2128+1?265)?(2127?1) = 2127+2?2652^{127}+2-2^{65}2127+2?265 > 0,所以可能溢出。
3.60
long loop(long x,int n) {long result = 0;long mask;for(mask = 1;maks != 0;mask=mask << (n % 64))//如果這里不能保證是正余數(0-63)的話,就用下面的寫法{result |= (x & mask);}return result; }這里難點主要在于salq %cl, %rdx這里的移位量到底是多少,根據移位操作中的解釋,因為被移位數為64位二進制(26=642^6 = 6426=64),所以只看%cl的低6位,或者循環的執行可以改為mask=mask<<(n & 0x3F)
3.61
首先看上圖c語句與其匯編語句的對應(3.6.6節),題目要求新函數對應的匯編代碼也會用到條件傳送,即要求有三目表達式。對于第4行,看起來可能是多余的,但3.6.6節講到條件傳送中,第一個操作數可以是源寄存器或者內存地址,所以立即數是不可以,所以這里多了一步。
如果函數改成long cread_alt(long *xp) { return (!xp ? 0 : *xp); },那么匯編代碼可能是:
cread_alt:movl $0, %eaxtestq %rdi, %rdicmovne (%rdi), %rax #直接傳送ret當然也可以改成如下:
long cread_alt(long *xp) {long t = 0;long *p = xp ? xp : &t; //得到xp指針或者0的地址,這句轉換為條件傳送語句后,也不會可能去讀取空指針return *p; //解引用,現在讀取指針指向值肯定不會出錯 }為了驗證匯編代碼,本人用MinGW進行了編譯,使用命令gcc -Og -S test.c,c文件內容為long cread(long *xp) { return (xp ? *xp : 0); },發現不管優化程度是多少,生成匯編基本都是(發現并沒有使用條件傳送,且沒怎么看懂):
LFB0:movl 4(%esp), %eax #得到了xp指針testl %eax, %eax je L3movl (%eax), %eax #指針不為空,讀取指針指向的值ret L3:xorl %eax, %eaxret3.62
鍛煉你的反向工程能力。注意有的語句可以簡化,不用非得照著匯編原封不動翻譯。
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
0000000000400590<switch_prob>:400590: 48 83 ee 3c sub $0x3c, %rsi #n -= 60,說明最后n的實際數要加60400594: 48 83 fe 05 cmp $0x5, %rsi #比較n > 5400598: 77 29 ja 4005c3 <switch_prob+0x33> #如果n > 5那么跳轉到default# 所以n <= 5的情況就只有交給跳轉表處理40059a: ff 24 f5 f8 06 40 00 jmpq *0x4006f8(,%rsi,8) #間接跳轉到0x4006f8 + 8*n# 跳到跳轉表對應的位置,從跳轉表來看,n的取值只能是0-5,因為只有6個八字節# 0和2會跳到這個位置4005a1: 48 8d 04 fd 00 00 00 lea 0x0(,%rdi,8),%rax4005a8: 00400593: c3 retq# 3會跳到這個位置4005aa: 48 89 f8 mov %rdi, %rax4005ad: 48 c1 f8 03 sar $0x3, %rax4005b1: c3 retq# 4會跳到這個位置4005b2: 48 89 f8 mov %rdi, %rax4005b5: 48 c1 e0 04 shl $0x4, %rax4005b9: 48 29 f8 sub %rdi, %rax4005bc: 48 89 c7 mov %rax, %rdi# 5會跳到這個位置4005bf: 48 0f af ff imul %rdi, %rdi# 大于5和1會跳到這個位置4005c3: 48 8d 47 4b lea 0x4b(%rdi), %rax4005c7: c3 retq而且從匯編代碼來看,如果n的值是<60,那么n-60<0,那么匯編代碼就會執行到jmpq *0x4006f8(,%rsi,8),本來應該跳轉到這6個八字節,但最終間接跳轉到非法的八字節。但也許此題重點不在于此,應假設n>=60.
long switch_prob(long x, long n){long result = x;switch(n):{case 60:case 62:result = x * 8;break;case 63:result = result >> 3;break;case 64:result = (result << 4) - x;x = result;case 65:x = x * x;//注意64,65后面沒有breakdefault:result = x + 75;} }3.64
假設有數組D[S][T]D[S][T]D[S][T],等式3.1為D+L(T?i+j)D+L(T \cdot i+j)D+L(T?i+j),這里T明顯為列數,更加深入的說,代表第一維度中每個維度的元素個數。
假設有數組D[R][S][T]D[R][S][T]D[R][S][T],等式3.1應為D+L(ST?i+T?j+k)D+L(ST \cdot i+ T \cdot j + k)D+L(ST?i+T?j+k),ST為第一維度中每個維度的元素個數。
則有:
S * T = 65 T = 13 S * T * R * 8 = 3640得到:R = 7 ; S = 5 ; T = 13
3.65
.L6:movq (%rdx), %rcx # t1 = A[i][j]movq (%rax), %rsi # t2 = A[j][i]movq %rsi, (%rdx) # A[i][j] = t2movq %rcx, (%rax) # A[j][i] = t1addq $8, %rdx # A[i][j] -> A[i][j+1]addq $120, %rax # A[j][i] -> A[j+1][i], 120 == 8*Mcmpq %rdi, %rax jne .L6 # if A[j][i] != A[M][M]A.從第6行就能看出來%rdx是A[i][j],因為每次只加8,即一個元素大小。
B.因為寄存器%rdx是A[i][j],所以另一個寄存器%rax是A[j][i]。
C.根據公式,120 == 8*M,所以M為15.
3.66
sum_col:leaq 1(, %rdi, 4), %r8 # %r8 = 4 * n + 1leaq (%rdi, %rdi, 2), %rax # result = 3 * nmovq %rax, %rdi # %rdi = 3 * ntestq %rax, %raxjle .L4 # if %rax <= 0, goto L4salq $3, %r8 # %r8 = 8 * (4 * n + 1)leaq (%rsi, %rdx, 8), %rcx # %rcx = A[0][j]的地址movl $0, %eax # result = 0movl $0, %edx # i = 0 .L3:addq (%rcx), %rax # result += A[i][j]addq $1, %rdx # i += 1addq %r8, %rcx # 這里每次+8*(4n+1),說明每一行有4n+1個,因此NC(n)為4*n+1cmpq %rdi, %rdx jne .L3 # 當%rdx等于3*n才循環結束,所以可以說明一共有3n行,因此NR(n)為3*nrep; ret .L4:movl $0, %eaxret所以有NR(n) = 3 * n; NC(n) = 4 * n + 1;
3.67
# strB process(strA s) # s in %rdi process:movq %rdi, %rax #第一個參數作為返回值,即要返回的結構體的開始地址movq 24(%rsp), %rdx #棧指針開始的第4個八字節的內容,存入%rdx,內容為結構體A的第二個成員:指針pmovq (%rdx), %rdx #讀取指針p指向的long型對象,再存入%rdxmovq 16(%rsp), %rcx #棧指針開始的第3個八字節的內容,內容為結構體A的成員數組的第2個元素:D[1]movq %rcx, (%rdi) #將D[1],存入返回結構體的第1個八字節movq 8(%rsp), %rcx #棧指針開始的第2個八字節的內容,內容為結構體A的成員數組的第1個元素:D[0]movq %rcx, 8(%rdi) #將D[0],存入返回結構體的第2個八字節movq %rdx, 16(%rdi) #將long型對象,存入返回結構體的第3個八字節#棧指針開始的第1個八字節,這里并沒有使用,因為存的是調用后的返回地址ret # long eval(long x, long y, long z) # x in %rdi, y in %rsi, z in %rdx eval:subq $104, %rsp #為棧分配了13*8字節空間,即13個八字節movq %rdx, 24(%rsp) #z存入棧指針開始的第4個八字節leaq 24(%rsp), %rax #棧指針開始的第4個八字節中的第一個字節的地址,存入%rax,作為結構體A的指針成員pmovq %rdi, (%rsp) #x存入棧指針開始的第1個八字節movq %rsi, 8(%rsp) #y存入棧指針開始的第2個八字節movq %rax, 16(%rsp) #p存入棧指針開始的第3個八字節leaq 64(%rsp), %rdi #棧指針開始的第9個八字節,的開始地址call process #這里有隱藏操作,分配八字節棧空間,存入返回地址,即下一行代碼地址movq 72(%rsp), %rax #這三行匯編執行加法addq 64(%rsp), %raxaddq 80(%rsp), %raxaddq $104, %rsp #回收棧空間retA.
注意此圖中,從下往上是地址增加方向。
B.
傳遞了%rsp+64,即棧指針開始的第9個八字節,的開始地址。
C.
因為結構參數s存在棧空間里,所以用%rsp+偏移量來訪問的。
D.
r的空間是分配在棧空間里,所以也是%rsp+偏移量來設置的。
E.
F.
結構體作為參數傳入和返回時,都是以指針來傳遞。
3.68
從匯編movslq 8(%rsi), %rax中,可以看出結構體str2中int t是從第2個八字節開始:
左邊為最大情況,右邊為最小情況。在最小情況中,如果數組再少一個元素,即數組大小由5字節變成4字節,那么int變量就會跑到第1個八字節中去了。
所以5<=B<=8.
從匯編addq 32(%rsi), %rax中,可以看出結構體str2中long u是從第5個八字節開始:
左邊為最大情況,右邊為最小情況。
所以7<=A<=10.
從匯編movq %rax, 184(%rdi)中,184既可能是最大情況,也可能是8字節補齊情況。
所以184-8<A*B*4<=184.
答案唯一解為:A=9; B=5;。
3.69
從c語句ap->x[ap->idx] = n;知道a_struct的兩個成員分別是數組和整數類型。
# void test(long i, b_struct *bp) # i in %rdi, bp in %rsi test:mov 0x120(%rsi), %ecx # bp+288 匹配bp->lastadd (%rsi), %ecx # bp->first + bp->lastlea (%rdi,%rdi,4), %rax # %rax = i*5lea (%rsi,%rax,8), %rax # %rax = bp+i*40# ap = &bp->a[i] = bp+8+i*40, +8意味著從bp開始的第1個八字節里面只有int,且a_struct大小必為8字節或更大,若為4字節,就不是+8而是+4了# 因為是i*40,所以a_struct大小為40字節# 此句很明顯取出了一個數,再結合倒數第二條指令mov %rcx, 0x10(%rax,%rdx,8),所以%rdx為ap->idx# 而且在結構體a_struct中,第一個成員為整數類型的idxmov 0x8(%rax), %rdxmovslq %ecx, %rcx # mov時符號拓展成4字8字節# 先看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的整數成員idxmov %rcx, 0x10(%rax,%rdx,8)# a_struct大小為40字節,第一個成員idx為long,8字節,還剩32字節# 第二個成員是long型數組,按照剩余字節,數組大小為4retqA.
因為7*40 + 8 = 288 = 0x120,所以CNT=7,要推出CNT必須先推理出a_struct的大小。
B.
3.70
proc:movq 8(%rdi), %rax #偏移量為8,存的是up->e1.y或者是up->e2.nextmovq (%rax), %rdx #用作內存引用,所以上面是up->e2.next,取出*(up->e2.next)的偏移量為0的內容,也有兩種情況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.ymovq %rdx, (%rdi) #將減法之差存入,up->e2.xretA.
e1.p 0 e1.y 8 e2.x 0 e2.next 8B.
16
C.
up->e2.x = *( *(up->e2.next).e1.p ) - *(up->e2.next).e1.y,具體看注釋。
3.71
這道題主要需要了解fgets函數(char * fgets ( char * str, int num, FILE * stream );)。下面將fgets函數的api文檔進行翻譯。
Reads characters from stream and stores them as a C string into str until (num-1) characters have been read or either a newline or the end-of-file is reached, whichever happens first.
A newline character makes fgets stop reading, but it is considered a valid character by the function and included in the string copied to str.
A terminating null character is automatically appended after the characters copied to str.
Notice that fgets is quite different from gets: not only fgets accepts a stream argument, but also allows to specify the maximum size of str and includes in the string any ending newline character.
從流中讀取字符,并將它們作為C string存儲進str參數中,直到num-1個字符已經被讀取,或者是到達新行或者EOF,這三個條件誰先到達都會使得讀取停止。
換行字符使得fgets函數停止讀取,不過換行符也會被當做一個合法字符來讀取。
一個空字符將會自動加在讀取的字符后,然后再復制給str。
On success, the function returns str.
If the end-of-file is encountered while attempting to read a character, the eof indicator is set (feof). If this happens before any characters could be read, the pointer returned is a null pointer (and the contents of str remain unchanged).
If a read error occurs, the error indicator (ferror) is set and a null pointer is also returned (but the contents pointed by str may have changed).
當函數執行成功,返回str。
當讀取字符時遇到一個EOF時,EOF標識符被設置。如果在任何字符都沒有進行讀取時,就發生了這樣的事,那么返回空指針(str指向的文本保持不變)。如果發生了讀取錯誤,那么error標識符被設置,也返回空指針(但str指向的文本可能會改變)。
1.根據翻譯得知,使用fgets函數便可以保證“當輸入字符超過緩沖區空間大小時,也能正常工作”。
2.關于“你的代碼還應該檢查錯誤條件,在遇到錯誤條件時返回”這點,其實判斷條件if (p == NULL)太籠統了,可以通過ferror函數(int ferror ( FILE * stream );)來判斷(stdin的類型是FILE *),當讀取出錯時,調用ferror函數返回非0值,上述代碼應寫成if ( (p == NULL) & (ferror(stdin) != 0) )。
3.72
此題與練習題3.49幾乎一模一樣,具體講解請看此篇博客。
注意c語句long **p = alloca(n * sizeof(long*));,p的類型為long **即long指針的指針,可以這么理解,分配long型數組時,返回long *指針;當分配long *型數組時,返回long **指針。
第5行%rax存的是30+8n。
第6行分為兩種情況:(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.
第8行加上偏置15(24?12^4-124?1),第9行 and -16,執行完這兩行,就相當于向上取整到16的倍數。注意在練習題3.49中,andq $-16, %r8這句是通過兩句匯編來實現的(先右移再左移,而本題是直接and -16)。
A.
s2=s1?((8?n+30)&0xfffffff0)s_2 = s_1 - ((8 * n + 30) \& 0xfffffff0)s2?=s1??((8?n+30)&0xfffffff0),根據上面的分析:
當n為偶數時,s2=s1?(8?n+16)s_2 = s_1 - (8 * n + 16)s2?=s1??(8?n+16)
當n為奇數時,s2=s1?(8?n+24)s_2 = s_1 - (8 * n + 24)s2?=s1??(8?n+24)
B.
p=(s2+15)&0xfffffff0p = (s_2 + 15) \& 0xfffffff0p=(s2?+15)&0xfffffff0
C.
大方向分為,當s2s_2s2?為16的倍數(這種情況p數組就直接從s2s_2s2?開始分配),和s2s_2s2?不為16的倍數(這種情況p數組還需要向地址增加方向滑動1-15個字節)。
1.因為e1和e2是用來滑動的,所以當e2為0,即s2s_2s2?為16的倍數時,當e1就會最大。再看當n為奇數時,分配數組空間為8 * n + 24,多出來24字節空間作為e1。e1最大為24,此時s2s_2s2?為16的倍數,且n為奇數。
2.當s2s_2s2?不為16的倍數時,p數組空間需要滑動來16對齊,當s2s_2s2?%16=1時,向地址增加方向滑動15個字節,此時達到最大滑動距離了,即e2=15。而e1=可滑動空間-e2,當n為偶數時,滑動空間為16字節,則e1=可滑動空間-e2=16-15=1。e1最小為1,此時s2s_2s2?%16=1,且n為偶數。
D.
p數組空間是16對齊的。
s2s_2s2?是容下8 * n字節的最小的16的倍數再加16。
3.73
原書中的匯編即圖3-51中的匯編,確實很亂,這樣改完之后清爽多了。
find_range:vxorps %xmm1, %xmm1, %xmm1vucomiss %xmm1, %xmm0jp .L1ja .L2jb .L3je .L4.L2:movl $2, %eaxret.L3:movl $0, %eaxret.L4:movl $1, %eaxret.L1:movl $3, %eaxrep; ret3.74
這樣的話,連cmovp都不需要用了。
find_range:vxorps %xmm1, %xmm1, %xmm1movq $0, %r8movq $1, %r9movq $2, %r10movq $3, %raxvucomiss %xmm1, %xmm0cmovb %r8, %raxcmove %r9, %raxcmova %r10, %raxret
可以看出在比較大于小于時,有兩套指令可以用,但因為比較浮點數用到的標志位為CF和ZF,所以再看上表,則應該使用下面這套指令。
3.75
A.
| 1 | %xmm0 | %xmm1 |
| 2 | %xmm2 | %xmm3 |
| 3 | %xmm4 | %xmm5 |
| n | %xmm(2n-2) | %xmm(2n-1) |
B.
imag部分返回值在%xmm1, real部分返回值在%xmm0.
總結
以上是生活随笔為你收集整理的《深入理解计算机系统》第三版 第三章家庭作业答案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈机器学习中的过拟合
- 下一篇: 我要换博客啦~Github+Hexo~W