# 2020q3 Homework2 (quiz2)
contributed by < `jonec76` >
###### tags: `sysprog-2020`
> [作業說明](https://hackmd.io/@sysprog/B1zAbkAEP)
> [題目](https://hackmd.io/@sysprog/2020-quiz2)
## 開發環境
```shell
$ uname -a
Linux jonec76-Aspire-M7720 5.4.0-47-generic #51-Ubuntu SMP GNU/Linux
```
## 參考解答
MMM = 0x8080808080808080
MASK = 0x40
AAA = 3
BBB = 6
XXX = 0xFFFFFFFFFFFFFFFF
YYY = M - 1
VV2 = 0
VV3 = -1
VV4 = 0x80
## 作業
- 重新回答 [第 2 周測驗題](https://hackmd.io/@sysprog/2020-quiz2),連同附帶的「延伸問題」。
- 將你的共筆加到 [2020q3 Homework2 (作業區)](https://hackmd.io/@sysprog/2020-homework2)
### Q1
- 分析提示程式碼
先從提示程式碼去思考加入 `if (str[i] & 0x80)` 的用意。此段程式碼 `is_ascii` 是為了判斷字串裡是否全都是 ascii code,判斷方法是檢測每個 char 對應到的 ascii 值是否小於 128。1 個 char 佔了 1byte,也就是說最大值只能存到 255,超過就是 overflow。
當 str[i] 是介於 128~255 時,就是 extended ascii。跟 `0x80` 做完 `&` 之後如果為 1,表示超過 127,此時則要回傳 `false` 表示該記憶體空間包含了非 ascii 的值。
- 解題想法
接著,看到題目需要改成一次比對 64bit (8 bytes)的資料,但提示程式碼只能比對 1 byte。這時其實只要改變比對的 bitmask 即可。也就是說,對於 `0x80` 做 8 次的比對,相當於將目前指向的後 8 個字元依序排列後,對 `0x8080808080808080` 做比對。
以表格來講,假如一個字串 "sysq3nice" 執行 `memcpy(&payload, str + i, 8);` 後,因為電腦是 little endian 的讀法,所以會像下面的表格
|index: |7|6|5|4|3|2|1|0|
|-----|-----|----|----|----|----|----|----|----|
|char:|c|i|n|3|q|s|y|s|
接著再和新的 bitmask 做 `&` 運算
|index: |7|6|5|4|3|2|1|0|
|-----|-----|----|----|----|----|----|----|----|
|char:|c|i|n|3|q|s|y|s|
|& op:|127|127|127|127|127|127|127|127|
當運算完之後,若這 8 個 bytes 有其中任何一個值大於 0,就表示這塊記憶體空間有 non-ascii 的值存在。
#### 補充筆記
- 如何得知自己的電腦是 little/big endian 呢?參考了 [how-can-i-find-endian-ness](https://stackoverflow.com/questions/8571089/how-can-i-find-endian-ness-of-my-pc-programmatically-using-c) 中看到以下的程式碼
```c=1
int num = 1;
if (*(char *)&num == 1)
printf("Little-Endian\n");
else
printf("Big-Endian\n");
```
先看 `(char *)&num`,每個 `int` 是 4 bytes,而當 `num` 為 1 時,little endian 會將這個 1 存在最低位的 byte,也就是 `(char *)&num`。如果是 big endian 的話會將 1 存在最高 byte 的位置,也就是 `(char *)&num+3` 的位置。
所以當取出最低位 byte `*(char *)&num` 時,如果是 1 就表示是 little endian。
另外,根據上述的方法嘗試做了下方的測驗
```c=1
int num = 1, b=2;
*((char *)&num+4) = 5;
printf("%d\n", b); // 5
```
更改 `num` 往後算第 5 個 byte 時,能夠更改到宣告在 `num` 之後的變數。
### Q2
```c=1
uint8_t hexchar2val(uint8_t in)
{
const uint8_t letter = in & MASK;
const uint8_t shift = (letter >> AAA) | (letter >> BBB);
return (in + shift) & 0xf;
}
```
- 思考方向
1. 第一個是 'A' 和 'a' 的輸出要相同
- 第 1 個想法可以觀察 `A` 跟 `a` 有何差別,`A` 是 `0x41 => 0100 0001`,`a` 是 `0x61 => 0110 0001`,也就是要想辦法忽略第 6 個 bit。
2. `in` 不是英文,就是數字。
- 第 2 個想法是要分辨出英文跟數字,從變數名稱 letter 來看就可知道 MASK 應該要做到這件事,只要知道第 7 個 bit 是 0 或者 1 就可分辨,所以 MASK 應該填入 `0100 0000 => 0x40`。
- 解題想法
AAA 跟 BBB 當時寫的時候湊很久,只能透過下面的二進位數來觀察
|hex: |0011 0001|0110 0001|0100 0001|
|---|---|---|---|
|to val:|0000 0001|0000 1010|0000 1010|
英文的輸出值都是 10 以上,透過觀察可以知道第 4 個 bit 一定為 1,數字的輸出都是 10 以下,可知道第 4 個 bit 為 0。那分辨英文跟數字的 bit 如前面第 (2) 點推敲是第 7 個 bit。於是在 `AAA` 可以填入 3,表示將第 7 個 bit 直接挪到第 4 個 bit。
另外,第 (1) 點的想法是忽略第 6 個 bit,但是沒有類似的選項。於是當時先試著右移 6 bits,發現剛好能夠將答案湊出來,既符合轉換數字的結果,也剛好能算出轉換英文。
### Q3
- jemalloc 裡頭的快速乘法原理
在 [div.c](https://github.com/jemalloc/jemalloc/blob/dev/src/div.c) 裡頭說到以下數學式
$\dfrac{n}{d}$ = $\lfloor{(
{\dfrac{2^k+r}{d}}\cdot{\dfrac{n}{2^k}}
)}\rfloor$ , $where~~r=(-2^k)~mod~d$
展開後可以得到 $\dfrac{n}{d}$=$\lfloor{\dfrac{n}{d}}+
(\dfrac{r}{d})\cdot
(\dfrac{n}{2^k})
\rfloor$。
由於此式子左右兩邊需要成立
所以 $(\dfrac{r}{d})\cdot
(\dfrac{n}{2^k})< 1$ 必須成立才行,
又知道 $r=(-2^k)~mod~d$,表示 $\dfrac{r}{d} < 1$ 成立。
接著,如何保證 $(\dfrac{n}{2^k}) < 1$ 呢?
在 div.c 裡頭可以看到 `two_to_k = ((uint64_t)1 << 32)`,這裡的 `1<<32` 就是為的產生出一個比任何 `uint32_t` 還要大的值。
如此一來, $(\dfrac{n}{2^k}) < 1$ 就成立了。
- 題目問題
```c=1
#define M ((uint64_t)(UINT64_C(XXX) / (D) + 1))
```
看到 `XXX` 時,直觀認為應該是 `1<<64`,但這個 1 是用在哪呢?此題的數學式推倒應為
$\dfrac{n}{d} = \dfrac{n}{2^{64}}
\cdot
(\dfrac{2^{64}-1}{d} + 1), where~d=3$
展開後可以得到下面式子
$\dfrac{n}{d} = \lfloor\dfrac{n}{2^{64}}
\cdot
(\dfrac{2^{64}}{d} + \dfrac{2}{3})\rfloor$
這邊可以觀察出跟前面的推倒過程是一樣的,可以知道 $(\dfrac{n}{2^{64}}\cdot\dfrac{2}{3}) < 1$ 一定成立。此時看到這個 $\dfrac{2}{3}$ 的分子 $2$,才知道一開始題目的 $+1$ 目的(應該是湊出來的?),是能夠使此項小於 $1$。
### Q4
```c=1
bool divisible(uint32_t n)
{
return n * M <= M - 1/*YYY*/;
}
```
- 題目分析
從 `Q3` 可得到 `M` = $\
(\dfrac{2^{64}-1}{d} + 1)=(\dfrac{2^{64}+2}{3} )$,
接著,從答案回推數學式子
$n$ 的可能性有 $3n', 3n'+1, 3n'+2, where ~n=1,2,...$,
1. $n=3n':$
$3n' * (\dfrac{2^{64}+2}{3} )=n' * (2^{64}+2)$
2. $n=3n'+1:$
($3n'+1) * (\dfrac{2^{64}+2}{3} )=n' * (2^{64}+2) + (\dfrac{2^{64}+2}{3} )$
3. $n=3n'+2:$
($3n'+2) * (\dfrac{2^{64}+2}{3} )=n' * (2^{64}+2) + 2*(\dfrac{2^{64}+2}{3} )$
分析到這會覺得蠻神奇的,明明這些出來的值 "看似" 都比 `M-1` 大,怎麼會有小於的時候呢?
但這邊仔細觀察時,可以先從 UINT64_C($2^{64}-1$) 來看。在 `uint64_t` 的變數中,最大值應為 $2^{64}-1$,那超過會怎麼辦呢?也就是 overflow,$2^{64}$ 其實會得到 0 這個數值。
回到上述的數學推導過程,可以發現當 $n=3n'+1, n=3n'+2$,也就是沒辦法被 3 整除時,所得結果都會多出一項 $r*M, where~r=n\%3, M=(\dfrac{2^{64}+2}{3})$。
這項是此題的關鍵,因為 overflow 的關係,前項的 $n' * (2^{64}+2)$ 可以化減成 $n'*2$。所以無法整除 3 時,都會多出後面那項 $r*M$,換句話說,要整除 3 時都必須小於 $r*M, where~r=1,2$,才可得出此題答案是 $M-1$
### Q5
- 題目分析
```c=1
if (s[j] >= 'A' && s[j] <= 'Z')
s[j] ^= 1 << 5;
```
如果 char 大於 `A` 或小於 `Z`,就表示是個大寫的英文。參考 ascii code 表可以發現,`A` 跟 `a` 差別於第 6 個 bit(可參考上面的 `Q2`)
- 思考方向
```c=1
uint64_t mask = ((A ^ Z) & PACKED_BYTE(0x80/*VV4*/)) >> 2;
```
若從這行倒著推測的話,`PACKED_BYTE(0x80)` 就是 `0x8080808080808080` ,`>>2` 剛好會是 `0x2020202020202020`,對應到每個 char 剛好是第 6 個 bit。
`(A ^ Z)` 的功用在哪呢?其實要能夠讓 `PACKED_BYTE(0x80) >> 2` 的關鍵在於,`(A ^ Z) = 0x8080808080808080` 才成立。
```c=1
uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + 0);
uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' - 1);
```
下圖是以每個 `*chunk` 的 byte 來看,列出所有可能
|range|*chunk[i] < A|A < *chunk[i] < Z|z < *chunk[i] |
|-|-|-|-|
|A 第 8 bit|0|1|1|
|Z 第 8 bit|0|0|1|
可以明顯看到,當 `A < *chunk[i] < Z` 時,做完 `A ^ Z` 後就會得到 `0x8080808080808080`
### Q6
- 思考方向
看完解法後覺得這題蠻神奇的,老師上課時有提示 "兩個相同的數做 XOR 會等於 0",那三個數呢?
思考方向是使用真值表,將所有可能的狀態都列出來後,可以思考成出現 0,1,2 次,當出現 3 次時可以當作出現 0 次,真值表如下
|high|low|input|output (high bit)|output (low bit)
|-|-|-|-|-|
|0|0|0|0|0|
|0|0|1|0|1|
|0|1|0|0|1|
|0|1|1|1|0|
|1|0|0|1|0|
|1|0|1|0|0|
high/low 記錄著該數值過去出現過幾次,input 記錄著該數值是否是這次讀到的數字。有趣的是,整個 array 中的數值都是共用 lower, higher 變數,對變數做 XOR,難道不會互相影響嗎?
仔細思考後發現,並不會影響,因為要看的是所有的數字。以 1,1,1,2,3,3,3 來看,第 0 個 bit 總共有 6 個 1 ,第 1 個 bit 總共有 7 個 1,依據上述狀態執行後的確能得到 2。
狀態希望是 `0` -> `1` -> `2` 的形式,所以當值為 `3` 時,則要變成 `0`,從真值表能推出下列式子:
\begin{aligned}
lower&=lower\cdot\bar{higher}\cdot\bar{input}+\bar{lower}\cdot\bar{higher}\cdot{input}\\
&=\bar{higher} \cdot(lower\cdot\bar{input}+\bar{lower}\cdot{input})\\
&=\bar{higher}\cdot(lower\oplus{input})
\end{aligned}
\begin{aligned} higher&=\bar{lower}\cdot{higher}\cdot\bar{input}+\bar{lower}\cdot\bar{higher}\cdot{input}\\
&=\bar{lower} \cdot(higher\cdot\bar{input}+\bar{higher}\cdot{input})\\
&=\bar{lower}\cdot(higher\oplus{input})
\end{aligned}
$