owned this note
owned this note
Published
Linked with GitHub
# 2018q3 E03: review
## W2Q4
### 該如何修改,才能讓 func 得以變更到 main 裡頭 ptrA 的內含值?
```clike=
#include <stdio.h>
int B = 2;
void func(int *p) { p = &B; }
int main() {
int A = 1, C = 3;
int *ptrA = &A;
func(ptrA);
printf("%d\n", *ptrA);
return 0;
}
```
這時候就是指向指標的變數出場的時候了。要讓 ptrA 的內容成功被修改,必須要:
```clike=
#include <stdio.h>
int B = 2;
void func(int **p) { *p = &B; };
int main() {
int A = 1, C = 3;
int *ptrA = &A;
int **ptrB = &ptrA;
func(ptrB);
printf("%d\n", *ptrA);
return 0;
}
```
以下是 linux 對 List 的實做
```clike=
#define LIST_ENTRY(type) \
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
#define LIST_REMOVE(elm, field) do { \
if (LIST_NEXT((elm), field) != NULL) \
LIST_NEXT((elm), field)->field.le_prev = \
(elm)->field.le_prev; \
*(elm)->field.le_prev = LIST_NEXT((elm), field); \
} while (0)
```
可以看到在 LIST_ENTRY 中,有一個指標存放第一個元素,以及一個指向指標的指標。
第13行有利用 le_prev 來修改指標中的內容。 把下一個的 le_prev 指到這個 node 的 le_prev ,再把 list 接好。
## W2Q3
### Linux 核心程式碼 include/linux/list.h 提到以下程式碼,為何每個 head 使用時都要先加上 () 呢?
```clike=
#define list_for_each_prev(pos, head) \
for (pos = (head)->prev; pos != (head); pos = pos->prev)
```
因為運算子優先順序的關係,如果有人傳入```*list```等表示法,巨集直接展開會變成```*list->```又 -> 的優先權高於 * 便會造成非程式設計師所預期的結果。
```clike=
#define LIST_FOREACH(var, head, field) \
for ((var) = LIST_FIRST((head)); \
(var); \
(var) = LIST_NEXT((var), field))
```
這是在 queue.h 裡面走訪 queue 的function, 可以看到裡面每個```var```都有標上(),甚至 head 也是因為巨集的關係用了兩層括號,避免運算子順序的問題。
## W3Q1
### 考慮以下程式碼:
```C=
#include <stdio.h>
#include <stdint.h>
struct test {
unsigned int x : 5;
unsigned int y : 5;
unsigned int z;
};
int main() {
struct test t;
printf("Offset of z in struct test is %ld\n",
(uintptr_t) &t.z - (uintptr_t) &t);
return 0;
}
```
在 GNU/Linux x86_64 環境中執行,得到以下輸出:
```
Offset of z in struct test is 4
```
倘若將第 10 和第 11 換為以下:
```C=10
printf("Address of t.x is %p", &t.x);
```
會發生什麼事?
根據 C99 6.7.2.1 的註解 106
> The unary & (address-of) operator cannot be applied to a bit-field object; thus, there are no pointers to or arrays of bit-field objects.
Address of operator 在這裡是不合法的。
用 gcc compile 的結果
> test.c:12:36: error: cannot take address of bit-field ‘x’
printf("Address of t.x is %p", &t.x);
>
那真是奇怪,為什麼上面可以通過編譯,下面卻不行呢?
會不會是沒有對齊的關係呢,如果把兩個 bitfield 剛好設成 32bits 會怎樣呢,是不是就對齊然後可以存取了呢?
```clike=
#include <stdio.h>
#include <stdint.h>
struct test {
unsigned int x : 32;
unsigned int y : 32;
unsigned int z;
};
int main() {
struct test t;
printf("Offset of z in struct test is %ld\n",
(uintptr_t) &t.z - (uintptr_t) &t);
printf("Address of t.x is %p", &t.x);
return 0;
}
```
結果還是編譯不過,遇到同樣的錯誤訊息。所以對 bitfield object 是無法 dereference 的。
[這邊](https://tonybai.com/2013/05/21/talk-about-bitfield-in-c-again/)有個很有趣的討論。
一般來說,```short``` 只佔 16bits,那如果出現
```clike=
struct short_field {
unsigned short a : 7,
b : 10;
};
```
剛好比 short 大小大了 1bit,編譯器會如何分配記憶體位置呢?
這邊很有技巧的用了一個 union 來存取個別 bit 的值
```clike=
void dump_native_bits_storage_layout(unsigned char *p, int bytes_num)
{
union flag_t {
unsigned char c;
struct base_flag_t {
unsigned int p7:1,
p6:1,
p5:1,
p4:1,
p3:1,
p2:1,
p1:1,
p0:1;
} base;
} f;
for (int i = 0; i < bytes_num; i++) {
f.c = *(p + i);
printf("Adress: %x ",p + i);
printf("%d%d%d%d %d%d%d%d \n",
f.base.p7,
f.base.p6,
f.base.p5,
f.base.p4,
f.base.p3,
f.base.p2,
f.base.p1,
f.base.p0);
}
printf("\n");
}
```
雖然這邊 f 的大小依舊是 4bytes,但是因為 bitfield 會緊密排列的緣故,所以可以取得個別 bits 的值。
```clike=
int main() {
struct short_field sf;
printf("%ld\n",sizeof(sf));
memset(&sf, 0, sizeof(sf));
sf.a = -1;
sf.b = -1;
int i = 999;
dump_native_bits_storage_layout((unsigned char *) &sf,sizeof(sf));
dump_native_bits_storage_layout((unsigned char *) &i,sizeof(i));
return 0;
}
```
輸出結果如下
```
Adress: e9ff776c 1111 1111
Adress: e9ff776d 1100 0000
Adress: e9ff776e 1111 1110
Adress: e9ff776f 0000 0000
Adress: e9ff7768 1110 0111
Adress: e9ff7769 1100 0000
Adress: e9ff776a 0000 0000
Adress: e9ff776b 0000 0000
```
可以看到 兩個 -1 都以 little endian 的形式把 4 bytes 切成兩半來存放 -1,也可以看到上面 integer 999 也以 little endian 的方法存放。
## W2Q5
### 測驗 `5`
- [ ] 認領人: ian910297
以下程式是合法 C99 程式嗎?
```C
#include <stdio.h>
int main() { return (********puts)("Hello"); }
```
請搭配 C 語言規格書解釋
根據C99規格書 6.5.3.2 第四點表示
> The unary * operator denotes indirection. If the operand points to a function, the result is a function designator;
>
又根據 C99 6.3.2.1 表示
>a function designator with type ''function returning type'' is converted to an expression that has type ''pointer to function returning type''.
>
所以 pointer to function 就變成 function designator ,然後 function designator 又被轉成 pointer to function ... 無限循環
繼續思考以下是否合法:
```C
#include <stdio.h>
int main() { return (**&puts)("Hello"); }
```
根據 C99 6.5.3.3 註解 87 提到
> 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.
>
所以 *&puts 還是 function designator ,再 dereference 當然還是 function designator
繼續變形:
```C
#include <stdio.h>
int main() { return (&**&puts)("Hello"); }
```
也會合法嗎?為什麼?請翻閱 C 語言規格書解說。
根據 C99 6.5.3.3 註解 87 提到
> Thus, &*E is equivalent to E (even if E is a null pointer)
>
所以基本上還是合法。
## W3Q2
### 測驗 `2`
考慮以下程式碼,在 little-endian 硬體上,會返回 `1`,反之,在 big-endian 硬體上會返回 `0`:
```C
int main() {
union { int a; char b;
} c = { .a = K1 };
return c.b == K2;
}
```
補完上述程式碼。
==作答區==
K1 = ?
* `(a)` 0
* `(b)` 1
* `(c)` -1
* `(d)` 254
K2 = ?
* `(a)` 0
* `(b)` 1
* `(c)` 254
:::success
延伸問題: 解釋運作原理,並找出類似的程式碼
:::
![](https://i.imgur.com/ixrndeO.png)
讀 char 時,只會用到一個 byte,只會用到圖中 a 這個byte,如果是 little endian 的裝置,擺放數字的順序會讓 1 在 a 記憶體位置中,而 bit endian 會把 1 擺在 a+3 這個記憶體位址裡面,便會造成數值差異。
以下是更簡潔的程式碼來自[stackoverflow](https://stackoverflow.com/questions/2100331/c-macro-definition-to-determine-big-endian-or-little-endian-machine/2100549#2100549)
```clike=
#define IS_BIG_ENDIAN (!*(unsigned char *)&(uint16_t){1})
```
將 1 轉成 uint16_t 型態,再取 address of ,在強制轉換成 unsigned char pointer ,在 dereference 來看是否為 1,是的話會傳 0 ,否的話回傳 1。