2018q3 Homework6
===
contributed by <`brad84622`, `amikai`, `AlecJY`>
# 題目 10
## solution
題目給定一段 atoi 的程式碼,並要求補完
補完如下,於下方說明
```clike=
#include<stdio.h>
#include <stdbool.h>
#include<string.h>
#include <stdint.h>
#define ULONG_MAX ((uint64_t)(~0UL)) /* 0xFFFFFFFF */
#define LONG_MAX (ULONG_MAX >> 1U) /* 0x7FFFFFFF */
#define LONG_MIN (~LONG_MAX) /* 0x80000000 */
static inline bool is_space(char c) { return ((c == ' ') || (c == '\t')); }
int64_t my_atoi(const char *str) {
const char *s = str;
char c;
uint64_t acc,cutlim,cutoff;
int neg = 0, any,base;
base = 10;
// Skip white space and pick up leading +/- sign if any.
do {
c = *s;
s++;
} while (is_space(c));
if (c == '-') {
neg = 1;
c = *s;
s++;
} else if (c == '+') {
c = *s;
s++;
} else { // No sign character
}
any = 0;
cutoff = (neg != 0) ? LONG_MIN : LONG_MAX;
cutlim=(cutoff%base);
acc=0;
cutoff = (LONG_MAX/base);
while ((c >= '0') && (c <= '9')) {
c=c-'0';
if((acc>cutoff)||((acc==cutoff)&&(c>cutlim))){
any=-1;
break;
}
else{
acc=acc*10;
acc=acc+c;
c=*s;
s++;
}
}
if (any < 0) {
acc = (neg != 0) ? LONG_MIN : LONG_MAX;
} else if (neg != 0) {
acc = ~acc + 1;
} else {
// There is no overflow and no leading '-' exists
}
return acc;
}
```
這段程式碼是將輸入的字串轉成整數型態,正負號由neg處理,故比較皆用正值,若overflow則給定any=-1並跳出迴圈,後續則會回傳最大(最小)值。
- 主要是填寫 while 區塊並對函數型態(`int 改成 int64_t`)進行修改
```clike=
while ((c >= '0') && (c <= '9')) {
c=c-'0';
if((acc>cutoff)||((acc==cutoff)&&(c>cutlim))){
any=-1;
break;
}
else{
acc=acc*10;
acc=acc+c;
c=*s;
s++;
}
}
```
- `if((acc>cutoff)...)`:因cutoff是最大值除以十,故當acc>cutoff時acc再乘以十必然會overflow;同理因cutlim是最大值mod10,當c>cutlim時acc乘以十加上c必然overflow。
- 考量到 64/32bit 皆要能運行, 以64bit為主對overflow的部份進行修改
```clike
cutoff = (cutoff/base); //在32bit底下負數除法會出錯
cutoff = (LONG_MAX/base); //改為正數避免此問題
```
# 題目 10
## solution
```clike=
struct s {
int field1;
char field2;
char field3;
};
int main() {
return (int) &((struct s *) 0)->field3;
}
```
在 gcc 搭配 64-bit GNU/Linux 編譯和執行,返回值會是什麼呢?請搭配 C 語言規格和編譯器最佳化來解釋原因,並且在 Linux 核心程式碼舉出類似用法。
先看 C 語言規格的兩句話:
```
An integer constant expression with the value 0, or such an expression cast to type void *,
is called a null pointer constant
```
所以 `null pointer` 在規格裡的定義就是 `void *`
```
Thus, &*E is equivalent to E (even if E is a null pointer), and &(E1[E2]) to ((E1)+(E2)).
It is always true that if E is a function designator or an lvalue that is a valid operand of the unary & operator, *&E is a function designator or an lvalue equal to E.
If *P is an lvalue and T is the name of an object pointer type, *(T)P is an lvalue that has a type compatible with that to which T points.
```
C 語言規格定義了 `&*E` 就是 `E` ( 就算是 null pointer 也是如此), 有趣的是那 `&(*E).m` 是什麼呢? C 語言規格裡並沒有提到
尷尬的是 `&((struct s *) 0)->field3` 可以替換成這樣
`&(*(struct s *) 0).field3` 也就是 `&(*E).m` 這種形態, 所以 C99 規格沒有提及到此種情況
其實這也是 linux kernel 在 `offsetof` 的實做方式
參考
- [Does &((struct name *)NULL -> b) cause undefined behaviour in C11?](https://stackoverflow.com/questions/26906621/does-struct-name-null-b-cause-undefined-behaviour-in-c11)
使用 gcc 編譯之後(使用O0), 使用反組譯來看看
```
Dump of assembler code for function main:
0x00000000000005fa <+0>: push %rbp
0x00000000000005fb <+1>: mov %rsp,%rbp
0x00000000000005fe <+4>: mov $0x5,%eax
0x0000000000000603 <+9>: pop %rbp
0x0000000000000604 <+10>: retq
```
gcc 實際的行為根本就沒有發生 dereference, 而是在編譯時期就已經把地址算好了
## offsetof
linux kernel 裡的 offsetof
```c=
// include/linux/stddef.h
#ifdef __compiler_offsetof
#define offsetof(TYPE, MEMBER) __compiler_offsetof(TYPE, MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#endif
// include/linux/compiler_types.h
#define __compiler_offsetof(a, b) __builtin_offsetof(a, b)
```
可以看到實做方式, 如果編譯器有定義就使用編譯器 `__builtin_offsetof` 沒有就使用 `((size_t)&((TYPE *)0)->MEMBER)`
在我的實做我會採用這種方式
```c=
#define offsetof(TYPE, MEMBER) ({ \
TYPE __dummy; \
(size_t)((char *)&__dummy.MEMBER - (char *)&__dummy);})
```
# 題目 11
## solution
``` C=
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
void ieee754_b32_encode(float x, char out[4]) {
bool sign = x < 0;
uint8_t exponent;
uint32_t fraction;
if (isinf(x)) {
exponent = 0xFF;
fraction = 0;
} else if (isnan(x)) {
exponent = 0xFF;
fraction = 0x7FFFFF;
} else {
if (sign)
x = -x;
int e = 0;
fraction = frexp(x, &e) * ((uint32_t) 2 << 23);
if (e <= 126) {
fraction = 0;
exponent = 0;
} else {
exponent = e + 126;
if (e + 126 > 0xFF) {
exponent = 0xFF;
fraction = 0;
}
}
}
out[0] = ((sign << 7) & 0x80) | ((exponent >> 1) & 0x7F);
out[1] = ((exponent << 7) & 0x80) | ((fraction >> 16) & 0x7F);
out[2] = (fraction >> 8) & 0xFF;
out[3] = fraction & 0xFF;
}
```
題目給了這段程式碼,說明這是將 IEEE 754 單精度浮點數轉換成特定 32-bit 表示法,並且要求寫出將轉換出的結果轉換回 IEEE 754 單精度浮點數的程式碼,還要指出原始程式碼的疏失。
首先先看一下原來的程式碼流程,這支程式碼輸入一個浮點數後會先確認浮點數的正負號,確認後會檢查浮點數是不是 infinity 或是 not a number ,如果是的話會直接做特殊處理。之後會將浮點數取絕對值,然後放進 `frexp()` 計算, `frexp()` 會回傳一個 0.5 ~ 1 之間的浮點數,以及整數 e ,使得 $原始值 = e^2 \times 回傳值$ ,之後將回傳值乘以 $2^{23}$ 轉成整數後的最低 23 bits 就是 mantissa,而 e 這個指數部分因為回傳值是 0.5 ~ 1 之間所以跟一般 IEEE 754 表示得到的指數部分多 1 ,接下來在第 20 行的比對我覺得有很大的問題, e 超過126的數字只有一點點,所以會導致大多數的浮點數最後輸出結果都一樣,達不到轉換表示法的目的,所以覺得這邊可能是題目要求指出的程式碼疏失之一,這邊的判斷式改成 `e <= -126` 會比較合理,雖然還是有點問題。第 24 行將 e 加上 126 算出 exponent ,並且判斷數字是否為無限大。最後一步是將剛剛算出來的 sign 、 exponent 、 fraction 依照跟 IEEE 754 一樣的規則,第一個 char array 的第一個 bit 放 sign , 後面接著放 exponent ,多的 bit 放進 array 的下一個元素,再將 faction 忽略最高位的那個 bit 後依照一樣的方式放進去。
再來就來看一下放進一些比較特別的數字所發生的事情,放入無限大和 NaN 由於一開始有做特殊處理,所以不會有問題,只是所有的 NaN 都被處理成 0x7FFFFF ,會遺失一些訊息。再來是放入 denormalized number , denormalized number 放進 `frexp()` 之後產生的 e 會是 -126 ,所以後面有檢察如果是的話將 exponent 和 fraction 設定為 0 ,但是在一般的 IEEE 754 裡面如果把 exponent 和 mantissa 都設為 0 代表的數字是 0 ,所以這邊把 fraction 改成其他數字會比較好。然後是放入 0 , `frexp()` 有提到如果放入 0 的話 e 和回傳值都會是 0 ,這在這段程式碼沒有特別處理,導致後面算出來的結果會讓 0 跟 0.5 的表示值一模一樣,所以 0 也需要特別判斷。
為了解決這些問題,將程式碼修改成以下這樣
``` C=
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
void ieee754_b32_encode(float x, char out[4]) {
bool sign = x < 0;
uint8_t exponent;
uint32_t fraction;
if (isinf(x)) {
exponent = 0xFF;
fraction = 0;
} else if (isnan(x)) {
exponent = 0xFF;
fraction = 0x7FFFFF;
} else if (x == 0) {
exponent = 0;
fraction = 0;
} else {
if (sign)
x = -x;
int e = 0;
fraction = frexp(x, &e) * ((uint32_t) 2 << 23);
if (e <= -126) {
fraction = 1;
exponent = 0;
} else {
exponent = e + 126;
if (e + 126 > 0xFF) {
exponent = 0xFF;
fraction = 0;
}
}
}
out[0] = ((sign << 7) & 0x80) | ((exponent >> 1) & 0x7F);
out[1] = ((exponent << 7) & 0x80) | ((fraction >> 16) & 0x7F);
out[2] = (fraction >> 8) & 0xFF;
out[3] = fraction & 0xFF;
}
```
接下來是將編碼過的數字轉換回浮點數的轉換程式,因為實際上格式跟普通的 IEEE 754 沒什麼差別,所以可以直接把每個 byte 都塞進 float 裡面就轉換回來了。
``` C=
#include <stdint.h>
float ieee754_b32_decode(char in[4]) {
float out;
uint32_t *pout = (uint32_t *) &out;
*pout = ((uint32_t) in[3]) & 0xFF;
*pout |= ((uint32_t) in[2] & 0xFF) << 8;
*pout |= ((uint32_t) in[1] & 0xFF) << 16;
*pout |= (uint32_t) in[0] << 24;
return out;
}
```