owned this note
owned this note
Published
Linked with GitHub
# 2024q1 Homework5 (assessment)
contributed by <`fatcatorange` >
## 因為自動飲料機而延畢的那一年
在這篇文章中,我首先看到了現實的一面,我一直有一個做出讓很多人使用的產品的夢想,因此我在大學、研究所期間學習了遊戲設計、網頁前後端等領域,但這篇文章和我的實作過程中都發現,在缺乏資金和實力真的不到頂尖的情況下,真的很難成功。
但看到一半時,我不禁佩服起了這些人,即使遇到這樣的困難還是堅持做下去,換做是我,看到第一個掉杯機花那麼久的時間作不出來,我應該就果斷放棄了,這大概也是我這麼爛的原因,因為以前失敗的經驗,遇到困難總是先覺得自己辦不到。
「你最大的問題在太害怕失敗了,既然都已經決定要延畢做飲料機了,那就要好好做,才不會辜負當初自己的期望」 這句話我已經在我的第一個作業共筆中看過類似留言,我真的很怕失敗,我也不知道怎麼辦,當我想做好一件事,真的開始做時又會顧慮東顧慮西,怕沒辦法在 deadline 前做出來、怕其他課或事情被拖延,到最後甚麼都做不好。
後半部分,大部分就是他們完成飲料機的過程,大部分我看不懂,因為牽涉到一些電路之類的東西,最後完成的結果也沒辦法商用化,但作者似乎還是覺得很滿意了。
作者提到,資工系的不會寫程式,電工系不會焊電路,這我看完文章之後非常有體悟,我常常刷題,自以為程式能力還可以,但真的要把這些程式能力拿去應用,我肯定不行,我甚至看不懂他用 nodejs 控制電路板的部分,這樣根本不能稱作會寫程式。
「儘管世界如此殘酷,但人卻不一樣,當你真心想做到一件事,付出足夠的犧牲,這個世界會聽見並做出回應,周遭的人漸漸願意相信你、花時間幫助你,你的付出並不見得會有結果,但是加上許多人的幫助,可能一切就不一樣了。」,也許是我還沒有付出足夠多的努力,這段話我沒有感覺到共鳴。
## 1~6 周教材研讀
### 你所不知道的 C 語言: linked list 和非連續記憶體
各名詞的解釋:
* object: 程式執行期間資料儲存的區域都可以稱為 object
* 所有儲存資料的區域都是,包含指標 (指標儲存的就是這個變數儲存的位址,自然也是物件。
* C 永遠是 call by value
* type: 根據規格書 6.2.5 描述:
```
The meaning of a value stored in an object or returned by a function is determined by the
type of the expression used to access it.
```
物件怎麼儲存或函式的回傳由 type 決定。
* 所有儲存資料的區域都是,包含指標 (指標儲存的就是這個變數儲存的位址,自然也是物件。
* 算術和指標 type 統稱為 scale type,可使用 i++ 等操作。
* 陣列、structure 等則稱為 aggregate type。
* 如果不知道物件大小,稱為 imcomplete type,這種 type 可以宣告指標,但不能建立實體 (也就是有宣告,但沒有定義裡面的樣子)。
* function, array, pointer 實際上都是 derived declarator types ,實質上都是指標。
練習題:
```c
*(int32_t * const) (0x67a9) = 0xaa6;
```
透過(int32_t * const) (0x67a9) , 0x67a9 ~~被轉為 32 bit 的型態(指標是 32 bit 儲存的)~~ 轉為指向 int32_t 類型的指標,之後透過 * 符號,將這個指標內的值修改為 0xaa6。
:::warning
因為老家沒有 linux 電腦,先使用線上編譯器嘗試:
```c
#include <stdio.h>
#include <stdint.h>
int main() {
*(int32_t * const) (0x67a9) = 0xaa6;
printf("%d", *(int32_t * const) (0x67a9));
return 0;
}
```
出現 segmentation fault, 之後再使用 linux 電腦嘗試。
仍出現 segmentation fault,使用 gdb 檢查:
```shell
main () at test.c:5
5 *(int32_t * const) (0x67a9) = 0xaa6;
(gdb) x 0x67a9
0x67a9: Cannot access memory at address 0x67a9
```
gdb 無法檢查這個位址,我嘗試先分配一塊空間出來:
```c
int32_t *temp = malloc(sizeof(int));
```
`$1 = (int32_t *) 0x5555555592a0`
接下來我嘗試直接寫入這個區域:
```c
*(int32_t * const) (0x5555555592a0) = 0xaa6;
printf("%d", *(int32_t * const) (0x5555555592a0));
```
```shell
6 int32_t *temp = malloc(sizeof(int));
(gdb) n
7 *(int32_t * const) (0x5555555592a0) = 0xaa6;
(gdb) n
8 printf("%d", *(int32_t * const) (0x5555555592a0));
(gdb) print *(0x5555555592a0)
$1 = 2726
(gdb) n
9 return 0;
(gdb) print *temp
$2 = 2726
```
結論是,真的可以直接寫資料進固定位址,但前提是程式必須能控制那會記憶體區域,我不理解這樣的作法會在什麼樣的場合用到(或許可以省下指標的空間?)
:::
* *void
*void 透過讓使用者必須強制轉型才能存取
舉例來說,下面的程式碼會出現錯誤告知嘗試 dereference void pointer:
```c
int main() {
int a = 1;
void *tmp = &a;
printf("%d",*tmp);
return 0;
}
```
但這樣可以正常執行(因為有強制轉型為指向 int 的指標)
```c
int main() {
// Write C code here
int a = 1;
void *tmp = &a;
printf("%d",*(int*) tmp);
return 0;
}
```
指標的指標:
當我們希望在函式中改變一個指標指向的位置,因為 c 是 call by value, 因此只會傳入這個指標的複製進去,在函式內僅會對這個複製的指標修改。
因此,透過指標的指標,複製一個指向某個位址的指標,這樣就可以真的修改這個指標指向的位址。
forward declaration 搭配指標的技巧:
前面提到 imcomplete type 可以宣告指標,因此如果宣告一個 struct, 並在其他函式中透過指標操作,不管之後這個 struct 怎麼修改都不會影響這個函式。
Pointers vs. Arrays
兩者不能切換的情況:
* extern char x[ ] != extern char *x
* char x[10] != char *x (char *x = malloc(10 * sizeof(char)) 也不行嗎?)
function array:
可以宣告一個函式,並讓一個指標指向他:
```c
#include <stdio.h>
void call(int a, int b) {
printf("%d", a * b);
}
int main() {
void (*callJimmy)(int,int);
callJimmy = call;
callJimmy(20,4);
}
```
透過 typedef ,可以定義特定樣子(包含的參數、回傳的型態)的函式,舉例來說:
```c
#include <stdio.h>
void call(int a, int b) {
printf("%d", a * b);
}
typedef int (*callJimmy)(int,int);
int main() {
callJimmy fptr;
fptr = call;
fptr(20,4);
}
```
這裡 callJimmy 就被定義成一種指向 `回傳為 int,輸入是兩個 int 的函式` 的指標。
甚至可以把函式指標弄成陣列(陣列內也是函式指標,因此陣列是指標的指標?):
```c
#include <stdio.h>
void add(int a, int b) {
printf("%d", a + b);
}
void sub(int a,int b) {
printf("%d", a - b);
}
typedef void (*callJimmy)(int,int);
int main() {
callJimmy fptr[2] = {add,sub};
fptr[0](5,6);
fptr[1](5,6);
}
```
也可改寫成:
```c
int main() {
callJimmy fptr[2] = {add,sub};
(*(fptr))(5,6);
(*(fptr + 1))(5,6);
}
```
針對指標的修飾 (qualifier)
char * const pContent; 代表指向一個 char ,並且宣告後就不能修改
const char * pContent; 則代表指向一個 const char,可以改為指向其他 const char。
offsetof:
計算偏移量:
```c
struct ssj {
int sj;
float super;
};
typedef void (*callJimmy)(int,int);
int main() {
struct ssj *strong;
printf("%ld", offsetof(struct ssj, super));
}
```
結果為 4,如果傳入 sj 則為 0。
因為資料 sj 欄位佔了 4 byte , sj 是開頭, super 在 sj 後面,所以是 4。
## 你所不知道的 C 語言:數值系統篇
二進位轉換:
大寫和小寫英文字母只有一個 0100000 的差距
轉小寫:
('A' | ' ')、('a' | ' ') =>a
'A'=>1000001
' '=>0100000
__
1100001 => a
'a'=>1100001
' '=>0100000
__
1100001 => a
轉大寫:
('a' & '_')、('A' & '_')
'a'=>1100001
'_'=>1011111
--
1000001 => A
'A'=>1000001
'_'=>1011111
--
1000001 => A
大小顛倒:
('a' ^ ' ')、('A' ^ ' ')
'a'=>1100001
' '=>0100000
--
1100001 => a
'A'=>1000001
' '=>0100000
--
1100001 => a
xor swap(當記憶體資源稀少時可使用)
範例:
*x = 1001
*y = 1101
```c
void xorSwap(int *x, int *y) {
*x ^= *y; // x = 0100
*y ^= *x; // y = 1001
*x ^= *y; // x = 1101
}
```
避免 overflow:
(x + y)/2 可能造成 overflow
(x + y)/2
=>(x + y) >> 1 (右移 1 功能與 /2 相同)
=>(x ^ y + (x & y) << 1) >> 1 (x ^ y 是相加不進位,x & y = 1 代表要進位,左移 1 代表把進位的部份加一)
=>(x & y) + ((x ^ y) >> 1 )
0110 //6
1111 //15
0110 + 0100 = 1010 //10
省去迴圈
```c
int func(unsigned int x) {
int val = 0; int i = 0;
for (i = 0; i < 32; i++) {
val = (val << 1) | (x & 0x1);
x >>= 1;
}
return val;
}
```
假設以比較小範圍來看(假設只有 4 bits)
若 x 是 1100:
第一次迴圈時:
```c
val = (val << 1) | (x & 0x1); // val = 0000 | (1100 & 0001) => val = 0000
x >>= 1; //x = 0110
```
第二次:
```c
val = (val << 1) | (x & 0x1); // val = 0000 | (0110 & 0001) => val = 0000
x >>= 1; //x = 0011
```
第三次:
```c
val = (val << 1) | (x & 0x1); // val = 0000 | (0011 & 0001) => val = 0001
x >>= 1; //x = 0001
```
第三次:
```c
val = (val << 1) | (x & 0x1); // val = 0010 | (0001 & 0001) => val = 0011
x >>= 1; //x = 0000
```
可以發現,越前面被 (x & 0x1) 設定成 1 的位元,最後會被推到 val 越後面的位元,因此這個函式就是在進行反轉。
如何不用迴圈完成?
```c
new = num;
new = ((new & 0xffff0000) >> 16) | ((new & 0x0000ffff) << 16);
new = ((new & 0xff00ff00) >> 8) | ((new & 0x00ff00ff) << 8);
new = ((new & 0xf0f0f0f0) >> 4) | ((new & 0x0f0f0f0f) << 4);
new = ((new & 0xcccccccc) >> 2) | ((new & 0x33333333) << 2);
new = ((new & 0xaaaaaaaa) >> 1) | ((new & 0x55555555) << 1);
```
## 你所不知道的 C 語言: bitwise 操作
abs(n) => ((n>>31) ^ n) - (n>>31)
當 n 為正數,n>>31 為 0000..00,xor n 仍為 n
當 n 為負數,n>>31 為 1111..11,xor n 為 ~(n),再 - (-1)(即 1111..11) 就是 abs(n)
set a bit:
`a |= (1<<n)`
不管原本該位元是 0 or 1,or 後結果都是 1
clear a bit:
`b &= ~(1 << n);`
假設要 clear 第3個 bit:
1<<3=>000...01000 => ~(1 << 3) => 111...10111
因此除了第三位外,其餘位元保留,而第三位因為是 0 ,& 的結果必定為 0。
toggle a bit:
`c ^= (1 << n)`
10011011
00010000
--
10001011 -> 原本是 1, xor 完變 0 ,否則變 1。
## 你所不知道的 C 語言:記憶體管理、對齊及硬體特性
heap 和 stack(部份參考 [記憶體分配:stack與heap](https://hackmd.io/@Ben1102/B1gfGLT3u)):
stack:
用來儲存 function 的呼叫、傳入的參數或區域變數,stack 的大小是在編譯完成就固定了,因此如果 stack 使用的記憶體過多會發生 stack overflow。
heap:
heap 則是動態分配的記憶體區域,如使用 malloc()等方法動態分配的,但需要注意分配的記憶體必須釋放,否則可能引起 memory leak。
memory leak 實驗:
文章中提到, memory leak 可能是由未釋放空間導致。
一段簡單的程式碼:
```c
void f(void)
{
void* s;
s = malloc(5000);
return;
}
int main(void)
{
while (1) f();
return 0;
}
```
這段程式碼中,f() 會不斷被呼叫,然而其申請的 5000 byte 空間不會被釋放就返回了,當程式執行一段時間後,因為沒辦法再從 heap 分配更多空間,因此會被強致結束。
而如果在返回前加入 free(s),則程式真的就會不段重複執行,因為每次都有把空間釋放,不會發生無法分配空間的問題。
gdb 的對齊實驗:
```shell
$1 = 0x5555555592a0 ""
(gdb) n
7 for (int i = 0; i < 10000; ++i) {
(gdb) print z
No symbol "z" in current context.
(gdb) n
9 z = malloc(sizeof(char));
(gdb) print z
$2 = 0x5555555592c0 ""
(gdb) n
7 for (int i = 0; i < 10000; ++i) {
(gdb) n
9 z = malloc(sizeof(char));
(gdb) print z
$3 = 0x5555555592e0 ""
```
:::warning
之後檢查 malloc 為何這樣分配?
:::
## 你所不知道的 C 語言:函式呼叫篇
:::danger
看不懂前面,先複習組合語言
:::
buffer overflow 實驗:
```shell
$ gcc -o bof -fno-stack-protector -g -no-pie test.c
```
-fno-stack-protector 是取消記憶體保護,-no-pie 是把讓程式從固定位置載入的功能取消,這樣攻擊比較容易成功。
下面可以看到回傳位址在 +62 處
```c
gdb-peda$ pd main
Dump of assembler code for function main:
0x0000000000401190 <+0>: endbr64
0x0000000000401194 <+4>: push rbp
0x0000000000401195 <+5>: mov rbp,rsp
0x0000000000401198 <+8>: sub rsp,0x10
0x000000000040119c <+12>: lea rax,[rip+0xe69] # 0x40200c
0x00000000004011a3 <+19>: mov rdi,rax
0x00000000004011a6 <+22>: call 0x401060 <puts@plt>
0x00000000004011ab <+27>: lea rax,[rbp-0xa]
0x00000000004011af <+31>: mov rdi,rax
0x00000000004011b2 <+34>: mov eax,0x0
0x00000000004011b7 <+39>: call 0x401080 <gets@plt>
0x00000000004011bc <+44>: lea rax,[rbp-0xa]
0x00000000004011c0 <+48>: mov rdi,rax
0x00000000004011c3 <+51>: call 0x401060 <puts@plt>
0x00000000004011c8 <+56>: mov eax,0x0
0x00000000004011cd <+61>: leave
=> 0x00000000004011ce <+62>: ret
End of assembler dump.
gdb-peda$
```
指向了 `aaaa...a`
```
0000| 0x7fffffffd8c8 ('a' <repeats 140 times>)
0008| 0x7fffffffd8d0 ('a' <repeats 132 times>)
0016| 0x7fffffffd8d8 ('a' <repeats 124 times>)
0024| 0x7fffffffd8e0 ('a' <repeats 116 times>)
0032| 0x7fffffffd8e8 ('a' <repeats 108 times>)
0040| 0x7fffffffd8f0 ('a' <repeats 100 times>)
0048| 0x7fffffffd8f8 ('a' <repeats 92 times>)
0056| 0x7fffffffd900 ('a' <repeats 84 times>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004011ce in main () at test.c:14
14 }
gdb-peda$ p $rsp
$1 = (void *) 0x7fffffffd8c8
gdb-peda$ x/g 0x7fffffffd8c8
warning: Unable to display strings with size 'g', using 'b' instead.
0x7fffffffd8c8: 'a' <repeats 140 times>
```
檢查 evil 函是的位址
```shell
gdb-peda$ p evil
$2 = {int ()} 0x401176 <evil>
```
透過文章方法,輸入 abcdef.... 來確認到回傳指令的偏移量:
```shell
egend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004011ce in main () at test.c:14
14 }
gdb-peda$ p $rsp
$1 = (void *) 0x7fffffffd8c8
gdb-peda$ x/s 0x7fffffffd8c8
0x7fffffffd8c8: "ttuvwxyzabcdefghijklmnop"
```
t 前面有 18 個字母,輸入 18 個 a 來完成:
```shell
echo -ne "aaaaaaaaaaaaaaaaaa\x76\x11\x40\x00\x00\x00\x00\x00"
aaaaaaaaaaaaaaaaaav@jason@jason-System-Product-Name:~/linux-2024/test$ echo -ne "aaaaaaaaaaaaaaaaaa\x76\x11\x40\x00\x00\x00\x00\x00" > payload
jason@jason-System-Product-Name:~/linux-2024/test$ ./bof < payload
Input:
aaaaaaaaaaaaaaaaaav@
程式記憶體區段錯誤 (核心已傾印)
```
失敗,透過 gdb 檢查:
```shell
gdb-peda$ x/s 0x7fffffffd8c8
0x7fffffffd8c8: "\\x76\\x11\\x40\\x00\\x00\\x00\\x00\\x00"
gdb-peda$ *p evil
Undefined command: "". Try "help".
gdb-peda$ p evil
$2 = {int ()} 0x401176 <evil>
```
問題似乎是因為字串被轉換了? 我輸入的 `\` 全部被轉成 `\\` 了。
我嘗試直接把 stack 的頭改成 0x401176 (evil 函是的位址)
```shell
gdb-peda$ p 0x7fffffffd8c8
$14 = 0x7fffffffd8c8
gdb-peda$ p *0x7fffffffd8c8
$15 = 0xf7c29d90
gdb-peda$ set *0x7fffffffd8c8 = 0x401176
gdb-peda$ p *0x7fffffffd8c8
$16 = 0x401176
```
出現以下錯誤:
```
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x7fff00401176
```
---
## 5/6 1對1 討論後研究:
### socket programming
因為覺得連最基本的 socket programming 都不熟悉,因此從此處開始研究:
以 [github 上的 socket programming 教學](https://github.com/davidleitw/socket) 作為主要教材:
#### UDP
##### 建立 socket:
```c
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
```
第一、二個參數分別代表 ipv4 和 udp 第三個是別名? 通常填入 0
建立好後,要設定 socket address ,因為這裡使用 ipv4,因此可以使用 `socket_in`來儲存資料。
例如可以這樣設定:
```
struct sockaddr_in serverAddr = {
.sin_family = AF_INET, //IPV4
.sin_addr.s_addr = INADDR_ANY, //不限 ip
.sin_port = serverPort
};
```
##### 綁定:
完成後要把這個 socket 的 fd 綁定給某個 ip 或 port,這裡需要注意的是因為 bind 欄位預設是要輸入 sockaddr,但我們使用的是 sockaddr_in ,因此要進行強致轉型( sockaddr_in 內有進行必要的填充,兩者大小相同。
```c
bind(socket_fd, (const struct sockaddr *)&serverAddr, sizeof(serverAddr));
```
##### 接收資料:
綁定完成後,可以使用 `recvfrom` 來接收資料,如下:
```c
if (recvfrom(socket_fd, buf, sizeof(buf), 0, (struct sockaddr *)&clientAddr, &len) < 0) {
break;
}
```
其中 clientAddr 和 serverAddr 相同,儲存一些 ip、port 之類的資料,這樣資料才能透過這些資訊回傳。
##### 傳輸資料:
使用 `sendto` 回傳資料,參數內容基本上和 recvfrom
```c
sendto(socket_fd, conv, sizeof(conv), 0, (struct sockaddr *)&clientAddr, sizeof(clientAddr));
```
也就是說, socket 綁定完成後通過 recvfrom 等待資料,並透過 sendto 函式進行回傳。
而 user 端就只要做跟 server 端相同的事,只是改為先傳輸,再等回傳。
奇怪的是,我原本以為程式執行到 recvfrom 就會卡住,收到資料後往下執行,然後迴圈再執行回 recvfrom 等待,因此我原本想法是如果 server 正在執行資料處理,那這時候其他用戶傳資料過來應該沒辦法收到,因此我做了以下實驗,先開啟 1 個 client,當 server 接收到資料後,先 sleep 10 秒鐘,而我就在這 10 秒鐘再開啟一個 client 端的程式傳輸資料,我預期應該只有第一個 client 會接收到資料,但實際上兩個 client 都有收到回覆
```c
sleep(10);
```
此外,我並沒有設定 client 端的資料,client 也沒有 bind 到某個 port,卻能成功執行?
檢查後發現是回傳到 `127.0.0.1:41173`,port 每次不同,這是代表原本就要 bind 到某個 port 嗎?
假設我有先進行綁定:
```c
struct sockaddr_in clientAddr = {
.sin_family = AF_INET, //IPV4
.sin_addr.s_addr = INADDR_ANY, //不限 ip
.sin_port = htons(54321)
};
if (bind(socket_fd, (const struct sockaddr *)&clientAddr, sizeof(clientAddr)) < 0) {
perror("Bind socket failed!");
close(socket_fd);
exit(0);
}
```
這次就成功抓到 port 54321 的資料,因此似乎在創建 socket 時,除非有綁定,否則就會自動分配一個 port 給他?
#### TCP
相比 UDP , TCP 必須建立連線,因此在 server 端要先透過 listen 進行監聽 (聽有沒有人要建立連線)
```c
int listen(int sockfd, int backlog);
```
`backlog` 代表最大連線數
在 client 端,則要使用 connect 進行連接,代連線建立完成就會被放到 complete connection queue。
```c
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
```
接下來使用 `access` 就可以獲取 complete connection queue 的資料(所以會被處理的資料都是已經建好連線了)
```c
int accept(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict addrlen);
```
### ktcp
https://hackmd.io/@fatCatOrange/ktcp
### RCU
關於 RCU 的筆記:https://hackmd.io/FxDhzUmKRnKKBl1RSFkpsg
ktime
stack 使用量?
1.2.4
MRE: https://en.wikipedia.org/wiki/Minimal_reproducible_example