---
tags: sysprog
---
# 2019q3 Homework5 (quiz7)
contributed by < `yxguo2536` >
### 測驗 `1`
考慮貌似簡單卻蘊含實作深度的 [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) 題目:
* 從 1 數到 n,如果是 3的倍數,印出 "Fizz"
* 如果是 5 的倍數,印出 "Buzz"
* 如果是 15 的倍數,印出 "FizzBuzz"
* 如果都不是,就印出數字本身
直覺的實作程式碼如下: (`naive.c`)
```cpp
#include <stdio.h>
int main() {
for (unsigned int i = 1; i < 100; i++) {
if (i % 3 == 0) printf("Fizz");
if (i % 5 == 0) printf("Buzz");
if (i % 15 == 0) printf("FizzBuzz");
if ((i % 3) && (i % 5)) printf("%u", i);
printf("\n");
}
return 0;
}
```
觀察 `printf` 的(格式)字串,可分類為以下三種:
1. 整數格式字串 `"%d"` : 長度為 2 B
2. "Fizz" 或 "Buzz" : 長度為 4 B
3. "FizzBuzz" : 長度為 8 B
考慮下方程式碼:
```cpp
#define MSG_LEN 8
char fmt[MSG_LEN + 1];
strncpy(fmt, &"FizzBuzz%u"[start], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
```
我們若能精準從給定輸入 `i` 的規律去控制 `start` 及 `length`,即可符合 FizzBuzz 題意:
```cpp
string literal: "fizzbuzz%u"
offset: 0 4 8
```
以下是利用 bit-wise 和上述技巧實作的 FizzBuzz 程式碼: (`bitwise.c`)
```cpp
#define MSG_LEN 8
static inline bool is_divisible(uint32_t n, uint64_t M) {
return n * M <= M - 1;
}
static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1;
static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1;
int main(int argc, char **argv) {
for (size_t i = 1; i <= 100; i++) {
uint8_t div3 = is_divisible(i, M3);
uint8_t div5 = is_divisible(i, M5);
unsigned int length = (2 << div3) << div5;
char fmt[MSG_LEN + 1];
strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
求 KK1 ~ KK3。
---
| div | (MSG_LEN >> KK1) >> (KK2 << KK3) |
| -- | -- |
| div3 = 1, div5 = 1 | 0 |
| div3 = 1, div5 = 0 | 0 |
| div3 = 0, div5 = 1 | 4 |
| div3 = 0, div5 = 0 | 8 |
我們先從 div3 = 1, div5 = 0 的情況去分析,因為最終結果為 0 , 我們知道 :
1. `(8 >> KK1) >> X` 等效於 `8 >> 4` ( 以 `8` 取代 `MSG_LEN`、以 `X` 代表 `KK2 << KK3` )。
因此 (KK1,X) 有可能是 (0,4) (1,3) (2,2) (3,1) (4,0)。 但其中 (1,3) (3,1) 很明顯是不可能的,因為光用 shift 算不出 3。
2. `(8 >> 4) >> 0` 是不可能的,因為 KK1 最大只到 2
3. `(8 >> 2) >> 2` 是不可能的,因為代表 KK1 只能是常數 2,但這樣結果最大也是 `(8 >> 2) >> 0` = 2,沒辦法表示 4 和 8 的結果。
4. 由刪去法知道,div3 = 1, div5 = 0 時運算式會是 `(8 >> 0) >> 4`
- **KK1 = 0 或 div5**
- (KK2 << KK3) 有可能是 (1 << 2) 或 (2 << 1),目前還無法確定順序。 但可以確定 **1 一定來自於 div3**,因為如果 1、2 都固定是常數,那麼算式 `(8 >> KK1) >> 4` 最大結果就是 1 ,不可能表達出 4 和 8 的結果。
現在,我們將算式選項縮減為`( 8 >> {0|div5} ) >> ( {2|div3} << {div3|2} )`,接下來從其他情況推敲 KK1 ~ KK3 :
5. 考慮到 div3 = 0, div5 = 0 結果為 8 : 根據第 4 點我們知道 KK1 只能是 0 。 但 `(8 >> 0) >> ( 2 << 0 )` 不等於 4,`(8 >> 0) >> ( 0 << 2 )` 才是。 所以 **KK2 = div3, KK3 = 2**
現在,我們知道算式為`( 8 >> {0|div5} ) >> ( div3 << 2 )`,繼續從其他情況推敲 KK1 :
6. 考慮到 div3 = 0, div5 = 1 結果為 4 : 我們知道 `(8 >> 0) >> (0 << 2)` 不會是答案,所以 **KK1 = div5**
算式為 `(MSG_LEN >> div5) >> (div3 << 2)`
這一題最大的陷阱應該是在 div3 = 1, div5 = 1 的情況,因為此時算式是 `(MSG_LEN >> 1) >> (1 << 2)` == `8 >> 5`,很直覺的會認為他不對,但其實不管是 `8 >> 5` 或是 `8 >> 4` 結果都是 0 ,所以也是對的。
### 測驗 `3`
[CS:APP 第 3 版](https://hackmd.io/@sysprog/S1vGugaDQ) 一書的第 3 章家庭作業 `3.70` (Page 224) 題目:
> 考慮到以下 union 宣告
```cpp
union ele {
struct {
long *p, y;
} e1;
struct {
long x;
union ele *next;
} e2;
};
```
> 編譯器產生以下組合語言程式碼:
```cpp
proc:
movq 8(%rdi), %rdx
movq (%rdx), %rax
movq (%rax), %rax
subq 8(%rdx), %rax
movq %rax, (%rdi)
ret
```
對應的 C 語言程式如下:
```cpp
void proc(union ele *ptr) {
ptr->AAA.x = *(ptr->BBB.CCC->DDD.p) - (ptr->EEE.FFF->GGG.HHH);
}
```
提示: 可用 `gcc -Os -S` 輸出來檢驗
請補完程式碼。
---
開始分析組語之前,我們可以先縮減一些選項 :
1. `AAA.x`,代表 `AAA` = **e2**, 因為一開始只有 struct e2 才有 x 成員
2. `DDD.p`,代表 `DDD` = **e1**, 因為一開始只有 struct e1 才有 p 成員
3. `CCC->e1.p`,代表 `CCC` = **next**,因為只有 next 後面有辦法接 `->` 運算子
4. `BBB.next.e1.p`,代表 `BBB` = **e2**, 因為只有 struct e2 才有 next 成員
5. `FFF->GGG`,代表 `FFF` = **next**,理由同 3
6. `EEE.next`,代表 `EEE` = **e2**, 理由同4
7. `next->GGG`,則 `GGG` 只可能是 e1 或 e2 ,因為前面是 `next->`
8. `{e1|e2}.HHH` 則 `HHH` 可以大膽參測是 y(當 `GGG` 是e1) 或 x(當 `GGG` 是e2),因為 p 和 next 兩者都是地址,不該拿來做減法運算。
整理一下,現在我們知道原程式碼應該是:
`ptr->e2.x = *(ptr->e2.next->e1.p) - (ptr->e2.next->{e1|e2}.{y|x})`
所以,真正的未知數其實只有 `GGG` 和 `HHH`
1. 先看最後一行 : `movq %rax, (%rdi)`,代表 dereference of %rdi == ptr->e2.x,換句話說, %rdi 存放的值是 x 的地址。
2. 回頭看第一行 : `movq 8(%rdi), %rdx`,因為 %rdi 是 x 的地址,所以 %rdx == *(&x+8) == next,更具體來說是 `ptr->e2.next`
3. 第二行: `movq (%rdx), %rax`,因為 %rdx 是 next, 所以 %rax 儲存的值是 *next,這邊有可能是 `next->e1.p` 或 `next->e2.x` (但根據接下來的第三行指令,我們知道它應該是代表 `next->e1.p`)
4. 第三行: `movq (%rax), %rax`,這邊又對 %rax 做了 dereference, 我們知道 %rax 有可能是 e1.p 或 e2.x ,但如果要對 (long) e2.x 做 dereference 說不過去,所以唯一的可能就是 `*(e1.p)`
根據1~4點的解析,我們知道組語第一到三行對應的 C 語言應為 `*(ptr->e2.next->e1.p)`
5. 第四行: `subq 8(%rdx), %rax`, 我們可以把它改寫成比較容易理解的形式 ` %rax = %rax - 8(%rdx) `,因為 %rax = `*(ptr->e2.next->e1.p)`, 所以我們知道 8(%rdx) = `ptr->e2.next->{e1|e2}.{y|x}`
6. 根據 2, 我們知道 %rdx 是 next,所以 8(%rdx) 等同於 *(next+8),而根據 3 我們知道 next 的地址應該是指向 e1, 所以 *(e1+8) == (*e1).y == e1->y
至此,我們知道 `GGG->HHH` = **e1->y**