3.58
/*
* x in %rdi, y in %rsi, z in %rdx
* subq %rdx, %rsi // y - z ==> y
* imulq %rsi, %rdi // x * y ==> x
* movq %rsi, %rax // y ==> %rax
* salq $63, %rax // << 63
* sarq $63, %rax // >> 63
* xorq %rdi, %rax // 這個時候的%rdi已經是x*y ^ %rax
* 因此可以得出結論 (x*y) ^ ((y-z) << 63 >> 63)
*/
long decode2(long x, long y, long z) {
return (x * y) ^ ((y - z) << 63 >> 63);
}
3.59
/*
根據提示:
x = 2^64 * x_h + x_l (x_h表示x的高64位,x_l表示x的低64位)
y = 2^64 * y_h + y_l (y_h表示y的高64位,y_l表示x的低64位)
x * y = (2^64 * x_h + x_l) * (2^64 * y_h + y_l)
= 2^64 * x_h * 2^64 * y_h + 2^64 * x_h * y_l + x_l * 2^64 * y_h + x_l * y_l
在上邊這個表示式中2^64 * x_h * 2^64 * y_h明顯已經越界,因此捨去,
x * y = 2^64(x_h * y_l + x_l * y_h) + (x_l * y_l)
上邊的公式很重要,它表達的就是x*y的乘積的樣式,根據p = 2^64 *p_h + p_l 再結合上邊的公式
我們得出的結論是:
2^64(x_h * y_l + x_l * y_h) + (x_l * y_l) = 2^64 *p_h + p_l
那麼2^64 *p_h = 2^64(x_h * y_l + x_l * y_h) + (x_l * y_l) - p_l
p_h = (x_h * y_l + x_l * y_h) + (x_l * y_l)/2^64 - p_l/2^64
(x_l * y_l)/2^64 表示相乘後右移64位正好是他們相乘後的高64位的值
p_l/2^64 則為0
因此我們就把任務簡化了,我們接下來看彙編
dest in %rdi, x in %rsi, y in %rdx
stroe_prod:
movq %rdx, %rax // %rax = y, 此時y_l = %rax
cqto // 該命令的作用是把%rax中的符號位擴充套件到%rdx中,此時y_h = %rdx
movq %rsi, %rcx // 這行命令的作用是配合下一行獲取x高64位的值
sarq $63, %rcx // 獲取x的高64的值x_h = %rcx
imulq %rax, %rcx // 計算y_l * x_h = %rax * %rcx
imulq %rsi, %rdx // 計算y_h * x_l = %rdx * %rsi
addq %rdx, %rcx // 計算x_h * y_l + x_l * y_h的值
mulq %rsi // 該命令是計算%rax * %rsi的值,也就是x_l * y_l的值
addq %rcx, %rdx // 根據上邊我們得出的結論,進行相加處理
*/
3.60
/*
我們先寫出彙編的註釋:
x in %rdi, n in %esi
loop:
movl %esi, %ecx // %ecx = n
movl $1, %edx // %edx = 1
movl $0, %eax // %eax = 0
jmp .L2 // 跳轉到L2
.L3:
movq %rdi, %r8 // %r8 = x
andq %rdx, %r8 // %r8 &= %rdx
orq %r8, %rax // %rax |= %r8
salq %c1, %rdx // %rdx <<= %cl
.L2:
testq %rdx, %rdx // %rdx & %rdx
jne .L3 // if != jump to .L3
根據.L2我們可以得出的結論是如果%rdx的值為0 就繼續迴圈
.L3中做了什麼事呢?
我們知道%rdx的初始值為1,返回值%rax的值為0,那麼.L3中的解釋為:
1. x &= %rdx
2. %rax |= x
3. %rdx << n的低8位的值,也是為了保護位移
通過分析,我們就可以得出結論,該函式的目的是得出x中n的倍數的位掩碼
答案:
A:
x --> %rdi
n --> %esi
result --> %rax
mask --> %rdx
B:
result = 0
mask = 1
C:
mask != 0
D:
mask <<= n
E:
result |= (x & mask)
F:
如下函式
*/
long loop(long x, int n) {
long result = 0;
long mask;
for (mask = 1; mask != 0; mask = mask << n) {
result |= (x & mask);
}
return result;
}
3.61
long cread(long *xp) {
return (xp ? *xp : 0);
}
long cread_alt(long *xp) {
return (!xp ? 0 : *xp);
}
3.62
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:
result = *p1 + *p2;
*p1 = result;
break;
case MODE_C:
*p1 = 59;
result = *p2;
break;
case MODE_D:
result = *p2;
*p1 = result;
result = 27;
break;
case MODE_E:
result = 27;
break;
default:
result = 12;
}
return result;
}
3.63
/*
sub $0x3c, %rsi // %rsi = n - 60
cmp $0x5, %rsi // 比較%rsi : 5
ja 4005c3 // 大於就跳轉
jmpq *0x4006f8(,%rsi,8) // 這一行的目的是直接在跳轉表中獲取地址然後跳轉
// 因此下邊這些彙編程式碼就是對應跳轉表中的地址
4005a1對應的index為0和2:
lea 0x0(,%rdi,8), %rax // result = 8x
4005c3對應的index為1,也就是case 1,通過觀察,它用的就是default的指令
所以case 1 在switch中是缺失的
4005aa對應的index為3:
mov %rdi,%rax // result = x
sar $0x3,%rax // result >>= 3
也就是result = x / 8
4005b2對應的index為4:
mov %rdi,%rax // result = x
shl $0x4,%rax // result <<= 4
sub %rdi,%rax // result -= x
mov %rax,%rdi // x = result
也就是result = x * 15; x = result
4005bf對應的index為5:
imul %rdi,%rdi // x *= x
lea 0x4b(%rdi), %rax // result = 75 + x
經過上邊的分析,就很容易得出結論了,但是別忘了要把index加上60
*/
long switch_prob(long x, long n) {
long result = x;
switch(n) {
case 60:
case 62:
result = 8 * x;
break;
case 63:
result = x / 8;
break;
case 64:
result = 15 * x;
x = result;
case 65:
x *= x;
default:
result = 75 + x;
}
return result;
}
3.64
設L為陣列元素的大小,X_a表示資料的起始地址
&A[i][j][k] = X_a + L(i * S * T + j * T + k)
我們再進一步分析彙編程式碼:
i in %rdi, j in %rsi, k in %rdx, dest in %rcx
leaq (%rsi,%rsi,2), %rax // %rax = 3j
leaq (%rsi,%rax,4), %rax // %rax = 13j
movq %rdi, %rsi // %rsi = i
salq $6, %rsi // 結合上一條指令,%rsi = i << 6
addq %rsi, %rdi // %rdi = 65i
addq %rax, %rdi // %rdi = 65i + 13j
addq %rdi, %rdx // %rdx = 65i + 13j + k
movq A(,%rdx,8), %rax // %rax = *(A + 8(65i + 13j + k))
movq %rax, (%rcx) // *dest = *(A + 8(65i + 13j + k))
movl $3640, %eax // %rax = 3640
使用A + 8(65i + 13j + k)和最上邊的公式對比後發現:
L: 8
T: 13
S: 5
要求出R還必須用到3640這個值
R * T * S * L = 3640
R = 3640 / 8 / 13 / 5 = 7
R: 7
3.65
我們先假設M為4,我們假設矩陣A為:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
那麼在用函式transpose處理之後,矩陣變成了
1 5 9 13
2 6 10 14
3 7 11 15
4 8 12 16
可以看出對矩陣沿著對角線進行了轉換。我們繼續看彙編程式碼
下邊的彙編程式碼只是函式中內迴圈中的程式碼
.L6:
movq (%rdx), %rcx // %rcx = A[i][j]
movq (%rax), %rsi // %rsi = A[j][i]
movq %rsi, (%rdx) // A[i][j] = A[j][i]
movq %rcx, (%rax) // A[j][i] = A[i][j]
addq $8, %rdx // %rdx += 8
addq $120, %rax // %rax += 120
cmpq %rdi, %rax //
jne .L6 //
我們很容易就發現,指向A[i][j]的暫存器為%rdx,指向A[j][i]的暫存器為%rax
求M最關鍵的是找出%rax暫存器移動的規律,因為%rdx也就是A[i][j] + 8 就表示右移一位
而%rax則要移動M * 8位
因此M = 120 / 8 = 15
上邊的暫存器%rdi應該放的就是i == j時的A[i][j]的地址
3.66
首先我們寫出彙編程式碼的註釋:
n in %rdi, A in %rsi, j in %rdx
sum_col:
leaq 1(,%rdi,4), %r8 // %r8 = 1 + 4n
leaq (%rdi,%rdi,2), %rax // %rax = 3n
movq %rax, %rdi // %rdi = 3n
testq %rax, %rax // 3n & 3n
jle .L4 // if <= 0 .L4
salq $3, %r8 // %r8 = (1 + 4n) << 3
leaq (%rsi,%rdx,8), %rcx // %rcx = 8j + A
movl $0, %eax // %rax = 0
movl $0, %edx // %rdx = 0
.L3:
addq (%rcx), %rax // %rax += *%rcx
addq $1, %rdx // %rdx += 1
addq %r8, %rcx // %rcx += (1 + 4n) << 3
cmpq %rdi, %rdx // %rdx : 3n
jne .L3
rep; ret
.L4:
movl $0, %eax // %rax = 0
ret
很明顯,.L3上邊的程式碼都是為迴圈準備資料的
如果n = 0 那麼就直接返回result = 0
然後初始化區域性變數%rdx儲存i的值,%rax儲存result的值,%rcx儲存每一行j的地址,
然後進入迴圈體.L3
由%rdx : 3n可以看出打破迴圈的條件是 i == 3n 推匯出:NR(n) = 3n
由%rcx += (1 + 4n) << 3可以看出,%rcx每次都移動了一行的寬度,也就是NC(n) = (1 + 4n) << 3
答案是:
NR(n) = 3n
NC(n) = (1 + 4n) << 3
3.67
我們先給彙編程式碼新增註釋:
x in %rdi, y in %rsi, z in %rdx
eval:
subq $104, %rsp // 給棧分配了104個位元組的空間
movq %rdx, 24(%rsp) // 把z的值儲存在偏移量為24的位置
leaq 24(%rsp), %rax // %rax儲存了z的指標
movq %rdi, (%rsp) // 把x的值儲存在偏移量為0的位置
movq %rsi, 8(%rsp) // 把y的值儲存在偏移量為8的位置
movq %rax, 16(%rsp) // 把z的指標值儲存在偏移量為16的位置
leaq 64(%rsp), %rdi // 把偏移量為64的指標賦值給%rdi,當做引數傳遞給後邊的函式
call process
movq 72(%rsp), %rax // 取出偏移量為72的值賦值給%rax
addq 64(%rsp), %rax // +
addq 80(%rsp), %rax // +
addq $104, %rsp // 恢復棧頂指標
ret
process:
movq %rdi, %rax // 把引數儲存到%rax
movq 24(%rsp), %rdx // %rdx = &z 這裡有點意思,當呼叫call後會把函式下邊程式碼的地址壓入棧中
movq (%rdx), %rdx // %rdx = z
movq 16(%rsp), %rcx // %rcx = y
movq %rcx, (%rdi) // 把y儲存到偏移量為64 + 8 = 72的位置
movq 8(%rsp), %rcx // %rcx = x
movq %rcx, 8(%rdi) // 把x儲存到偏移量為64 + 8 + 8 = 80的位置
movq %rdx, 16(%rdi) // 把z儲存到偏移量為64 + 8 + 16 = 88的位置
ret
通過上邊的註釋,下邊的問題就很清楚了
A:
----------- <-- 108
z
----------- <-- 24
&z
----------- <-- 16
y
----------- <-- 8
x
----------- <-- %rsp
B:
傳遞了一個相對於%rsp偏移量為64的指標
C:
直接使用偏移量來訪問的s的元素
D:
直接設定偏移量
E:
----------- <-- 108
----------- <-- 88
z
----------- <-- 80
x
----------- <-- 72
y
----------- <-- 64 --- %rax
----------- <-- 32
z
----------- <-- 24
&z
----------- <-- 16
y
----------- <-- 8
x
----------- <-- %rsp
F:
通過這個例子,我們能夠發現,如果把結構作為引數,那麼實際傳遞的會是一個空的位置指標,函式把資料
儲存在這個位置上,同時返回值也是這個指標。
3.68
p in %rdi, q in %rsi
setVal:
movslq 8(%rsi), %rax // %rax = *(8 + q)
addq 32(%rsi), %rax // %rax += *(32 + q)
movq %rax, 184(%rdi) //
這個問題算是非常簡單的,由最後一條程式碼再加上str的結構,我們可以得出這樣一個等式
4 * A * B + space = 184 由於對齊原則是保證8的倍數,分別假設space為7和0
==> 44 < A * B <= 46
%rax = *(8 + q) 可以推斷出char array[B] 應該總共使用8個位元組
因為需要考慮對齊原則,所以先得出 B <= 8
short s[A] %rax += *(32 + q)
我們t佔用4個位元組 ==> 4 + A * 2 <= 32 - 8 <= 24
於是我們有三個公式來做判斷:
44 < A * B <= 46
B <= 8
A <= 10
那麼A * B 的值只能是45 組合就是 5 * 9
由於 B <= 8 因此 B = 5 A = 9
我們再驗證一番,short s[A] 由於對齊原則 佔用了20個位元組,跟彙編程式碼一致
答案:
A = 9
B = 5
3.69
i in %rdi, bp in %rsi
test:
mov 0x120(%rsi), %ecx // %rcx = *(288 + bp)
add (%rsi), %ecx // %rcx = *(288 + bp) + *bp
lea (%rdi,%rdi,4), %rax // %rax = 5 * i
lea (%rsi,%rax,8), %rax // %rax = 5 * i * 8 + bp
mov 0x8(%rax), %rdx // %rdx = *((5 * i * 8 + bp) + 8)
movslq %ecx, %rcx
mov %rcx, 0x10(%rax,%rdx,8) // &(16 + %rax + 8 * %rdx) = %rcx
retq
由 %rdx = (5 * i * 8 + bp) + 8 可以推匯出 a_struct a[CNT] 每個元素佔40個位元組,first佔8個位元組
==>
CNT = (288 - 8) / 40 ==> CNT = 7
本題重點理解%rax 和 %rdx中儲存的是什麼的值,
%rax中儲存的是ap的值,而%rdx中儲存的是ap->idx的值,理解了這一層接下來就簡單了
說明ap->idx儲存的是8位元組的值,根據 &(16 + %rax + 8 * %rdx) = %rcx 可以得出idx應該是結構體的第一個變數long idx
如果結構體佔用了40個位元組 , 那麼陣列x應該佔用 40 - 8 也就是32個位元組,每個元素佔8個,可以容納4個元素
typedef struct {
long idx;
long x[4];
}a_struct;
這個題目最重要的地方是理解mov 0x8(%rax), %rdx 這段程式碼,它是求ap->idx的值。
3.70
A:
0
8
0
8
B:
e1最多需要16個位元組
e2最多需要16個位元組
因此 總共需要16個位元組
C:
up in %rdi
proc:
movq 8(%rdi), %rax // %rax = *(8 + up) 取出偏移量為8的地址
movq (%rax), %rdx // %rdx = *%rax 取出該地址中的值
movq (%rdx), %rdx // 取出指標指向的值
subq 8(%rax), %rdx // 用該值減去 *(%rax + 8)
movq %rdx, (%rdi) //
ret
一般來說 如果一個暫存器,比如說%rax 在下邊的使用中用到了(%rax),我們就認定該暫存器儲存的值為指標
movq 8(%rdi), %rax %rax儲存了up偏移量為8的指標值,在該函式中偏移量為8還是指標的只能是e2的next
==> %rax = up -> e2.next
movq (%rax), %rdx %rdx 同樣儲存的是指標,對(%rax)取值得到的是up下一個unio的指標
==> %rdx = *(up -> e2.next)
movq (%rdx), %rdx 這行程式碼過後,%rdx就不再是指標了,是一個值,但執行之前,%rdx是個指標
==> %rdx = *(*(up -> e2.next) -> e2.p)
subq 8(%rax), %rdx 我們知道%rax是個指標 指向next +8後
==> 8(%rax) = *(up -> e2.next) -> e1.y
答案:
up -> e2.x = *(*(up -> e2.next) -> e2.p) - *(up -> e2.next) -> e1.y;
3.71
#include <stdio.h>
#include <assert.h>
#define BUF_SIZE 12
void good_echo(void) {
char buf[BUF_SIZE];
while(1) {
/* function fgets is interesting */
char* p = fgets(buf, BUF_SIZE, stdin);
if (p == NULL) {
break;
}
printf("%s", p);
}
return;
}
int main(int argc, char* argv[]) {
good_echo();
return 0;
}
3.72
我們先畫一畫棧圖:
----------
---------- <-- %rbp 0
---------- s1 <-- -16
e1
----------
p
---------- p
e2
---------- s2
A:
s2 = %rsp - 16 - (-16 & (8n + 30)) 由於s2 = %rsp - 16 所以
s2 = s1 - (-16 & (8n + 30))
這裡的-16的十六進位制表示為0xfffffff0,之所以用& 就是為了求16的整數倍
B:
p = (s2 + 15) & 0xfffffff0
C:
s2 = s1 - (0xfffffff0 & (8n + 30)) 根據這個公式
當n是偶數的時候,我們可以把式子簡化為 s2 = s1 - (8 * n + 16)
當n是奇數的時候,我們可以把式子簡化為 s2 = s1 - (8 * n + 24)
先求e1最小的情況
e1和e2是對立的關係,要想e1最小,那麼e2就要最大,e2最大也就是15,
n是偶數的時候,e1 = 16 - 15 = 1 這個時候s1 % 16 == 1
e1最大的情況:
e2 == 0 時 e1最大, 當n是奇數的時候,e1 == 24 這個時候s1 % 16 == 0(p中多處了一個8位元組)
D:
s2 確保能夠容納足夠的p, p能夠保證自身16對齊
3.73
#include <stdio.h>
#include <assert.h>
typedef enum {NEG, ZERO, POS, OTHER} range_t;
range_t find_range(float x) {
__asm__(
"vxorps %xmm1, %xmm1, %xmm1\n\t"
"vucomiss %xmm1, %xmm0\n\t"
"jp .P\n\t"
"ja .A\n\t"
"jb .B\n\t"
"je .E\n\t"
".A:\n\t"
"movl $2, %eax\n\t"
"jmp .Done\n\t"
".B:\n\t"
"movl $0, %eax\n\t"
"jmp .Done\n\t"
".E:\n\t"
"movl $1, %eax\n\t"
"jmp .Done\n\t"
".P:\n\t"
"movl $3, %eax\n\t"
".Done:\n\t"
);
}
int main(int argc, char* argv[]) {
range_t n = NEG, z = ZERO, p = POS, o = OTHER;
assert(o == find_range(0.0/0.0));
assert(n == find_range(-2.3));
assert(z == find_range(0.0));
assert(p == find_range(3.33));
return 0;
}
3.74
#include <stdio.h>
#include <assert.h>
typedef enum {NEG, ZERO, POS, OTHER} range_t;
range_t find_range(float x) {
__asm__(
"vxorps %xmm1, %xmm1, %xmm1\n\t"
"movq $1, %rax\n\t"
"movq $2, %r8\n\t"
"movq $0, %r9\n\t"
"movq $3, %r10\n\t"
"vucomiss %xmm1, %xmm0\n\t"
"cmovg %r8, %rax\n\t"
"cmove %r9, %rax\n\t"
"cmovpq %r10, %rax\n\t"
);
}
int main(int argc, char* argv[]) {
range_t n = NEG, z = ZERO, p = POS, o = OTHER;
assert(o == find_range(0.0/0.0));
assert(n == find_range(-2.3));
assert(z == find_range(0.0));
assert(p == find_range(3.33));
return 0;
}
3.75
這個題考察的是複數的概念
複數 = 實數 + 虛數
傳參的時候,有這樣的規律
(複數1, 複數2, 複數3...) 對應的浮點暫存器就會是:
%xmm0, %xmm1, %xmm2, %x
總結
看本章的過程當中,彷彿回到了大學時光,在讀的的過程中,書本上的練習題做的還可以,但是感覺很多前邊講過的東西還是不太清楚,於是在讀完後又重新讀了一遍,在閱讀第二遍的過程中, 注意到了很多細節,比如之前push 和 pop 有點迷惑,現在就非常清晰了
要想記住書本中的內容,看來還是要多讀幾遍。我感覺在該章中學到最多的是理解了c語言在機器程式碼級別的表示,對資料在記憶體中的操作更加了解了,不得不感慨編譯器的強大,現在還感覺不出這些東西在實際工作中的用處,但對執行時棧的理解還是很有用的。
我已經把答案上傳到了我的github中深入理解計算機系統(第三版)作業題答案(第三章)
有錯誤的地方可以直接指出,歡迎討論。