contributed by <fatcatorange
>
在這篇文章中,我首先看到了現實的一面,我一直有一個做出讓很多人使用的產品的夢想,因此我在大學、研究所期間學習了遊戲設計、網頁前後端等領域,但這篇文章和我的實作過程中都發現,在缺乏資金和實力真的不到頂尖的情況下,真的很難成功。
但看到一半時,我不禁佩服起了這些人,即使遇到這樣的困難還是堅持做下去,換做是我,看到第一個掉杯機花那麼久的時間作不出來,我應該就果斷放棄了,這大概也是我這麼爛的原因,因為以前失敗的經驗,遇到困難總是先覺得自己辦不到。
「你最大的問題在太害怕失敗了,既然都已經決定要延畢做飲料機了,那就要好好做,才不會辜負當初自己的期望」 這句話我已經在我的第一個作業共筆中看過類似留言,我真的很怕失敗,我也不知道怎麼辦,當我想做好一件事,真的開始做時又會顧慮東顧慮西,怕沒辦法在 deadline 前做出來、怕其他課或事情被拖延,到最後甚麼都做不好。
後半部分,大部分就是他們完成飲料機的過程,大部分我看不懂,因為牽涉到一些電路之類的東西,最後完成的結果也沒辦法商用化,但作者似乎還是覺得很滿意了。
作者提到,資工系的不會寫程式,電工系不會焊電路,這我看完文章之後非常有體悟,我常常刷題,自以為程式能力還可以,但真的要把這些程式能力拿去應用,我肯定不行,我甚至看不懂他用 nodejs 控制電路板的部分,這樣根本不能稱作會寫程式。
「儘管世界如此殘酷,但人卻不一樣,當你真心想做到一件事,付出足夠的犧牲,這個世界會聽見並做出回應,周遭的人漸漸願意相信你、花時間幫助你,你的付出並不見得會有結果,但是加上許多人的幫助,可能一切就不一樣了。」,也許是我還沒有付出足夠多的努力,這段話我沒有感覺到共鳴。
各名詞的解釋:
物件怎麼儲存或函式的回傳由 type 決定。
練習題:
透過(int32_t * const) (0x67a9) , 0x67a9 被轉為 32 bit 的型態(指標是 32 bit 儲存的) 轉為指向 int32_t 類型的指標,之後透過 * 符號,將這個指標內的值修改為 0xaa6。
因為老家沒有 linux 電腦,先使用線上編譯器嘗試:
出現 segmentation fault, 之後再使用 linux 電腦嘗試。
仍出現 segmentation fault,使用 gdb 檢查:
gdb 無法檢查這個位址,我嘗試先分配一塊空間出來:
$1 = (int32_t *) 0x5555555592a0
接下來我嘗試直接寫入這個區域:
結論是,真的可以直接寫資料進固定位址,但前提是程式必須能控制那會記憶體區域,我不理解這樣的作法會在什麼樣的場合用到(或許可以省下指標的空間?)
但這樣可以正常執行(因為有強制轉型為指向 int 的指標)
指標的指標:
當我們希望在函式中改變一個指標指向的位置,因為 c 是 call by value, 因此只會傳入這個指標的複製進去,在函式內僅會對這個複製的指標修改。
因此,透過指標的指標,複製一個指向某個位址的指標,這樣就可以真的修改這個指標指向的位址。
forward declaration 搭配指標的技巧:
前面提到 imcomplete type 可以宣告指標,因此如果宣告一個 struct, 並在其他函式中透過指標操作,不管之後這個 struct 怎麼修改都不會影響這個函式。
Pointers vs. Arrays
兩者不能切換的情況:
function array:
可以宣告一個函式,並讓一個指標指向他:
透過 typedef ,可以定義特定樣子(包含的參數、回傳的型態)的函式,舉例來說:
這裡 callJimmy 就被定義成一種指向 回傳為 int,輸入是兩個 int 的函式
的指標。
甚至可以把函式指標弄成陣列(陣列內也是函式指標,因此陣列是指標的指標?):
也可改寫成:
針對指標的修飾 (qualifier)
char * const pContent; 代表指向一個 char ,並且宣告後就不能修改
const char * pContent; 則代表指向一個 const char,可以改為指向其他 const char。
offsetof:
計算偏移量:
結果為 4,如果傳入 sj 則為 0。
因為資料 sj 欄位佔了 4 byte , sj 是開頭, super 在 sj 後面,所以是 4。
二進位轉換:
大寫和小寫英文字母只有一個 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
避免 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
省去迴圈
假設以比較小範圍來看(假設只有 4 bits)
若 x 是 1100:
第一次迴圈時:
第二次:
第三次:
第三次:
可以發現,越前面被 (x & 0x1) 設定成 1 的位元,最後會被推到 val 越後面的位元,因此這個函式就是在進行反轉。
如何不用迴圈完成?
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。
heap 和 stack(部份參考 記憶體分配:stack與heap):
stack:
用來儲存 function 的呼叫、傳入的參數或區域變數,stack 的大小是在編譯完成就固定了,因此如果 stack 使用的記憶體過多會發生 stack overflow。
heap:
heap 則是動態分配的記憶體區域,如使用 malloc()等方法動態分配的,但需要注意分配的記憶體必須釋放,否則可能引起 memory leak。
memory leak 實驗:
文章中提到, memory leak 可能是由未釋放空間導致。
一段簡單的程式碼:
這段程式碼中,f() 會不斷被呼叫,然而其申請的 5000 byte 空間不會被釋放就返回了,當程式執行一段時間後,因為沒辦法再從 heap 分配更多空間,因此會被強致結束。
而如果在返回前加入 free(s),則程式真的就會不段重複執行,因為每次都有把空間釋放,不會發生無法分配空間的問題。
gdb 的對齊實驗:
之後檢查 malloc 為何這樣分配?
看不懂前面,先複習組合語言
buffer overflow 實驗:
-fno-stack-protector 是取消記憶體保護,-no-pie 是把讓程式從固定位置載入的功能取消,這樣攻擊比較容易成功。
下面可以看到回傳位址在 +62 處
指向了 aaaa...a
檢查 evil 函是的位址
透過文章方法,輸入 abcdef… 來確認到回傳指令的偏移量:
t 前面有 18 個字母,輸入 18 個 a 來完成:
失敗,透過 gdb 檢查:
問題似乎是因為字串被轉換了? 我輸入的 \
全部被轉成 \\
了。
我嘗試直接把 stack 的頭改成 0x401176 (evil 函是的位址)
出現以下錯誤:
因為覺得連最基本的 socket programming 都不熟悉,因此從此處開始研究:
以 github 上的 socket programming 教學 作為主要教材:
第一、二個參數分別代表 ipv4 和 udp 第三個是別名? 通常填入 0
建立好後,要設定 socket address ,因為這裡使用 ipv4,因此可以使用 socket_in
來儲存資料。
例如可以這樣設定:
完成後要把這個 socket 的 fd 綁定給某個 ip 或 port,這裡需要注意的是因為 bind 欄位預設是要輸入 sockaddr,但我們使用的是 sockaddr_in ,因此要進行強致轉型( sockaddr_in 內有進行必要的填充,兩者大小相同。
綁定完成後,可以使用 recvfrom
來接收資料,如下:
其中 clientAddr 和 serverAddr 相同,儲存一些 ip、port 之類的資料,這樣資料才能透過這些資訊回傳。
使用 sendto
回傳資料,參數內容基本上和 recvfrom
也就是說, socket 綁定完成後通過 recvfrom 等待資料,並透過 sendto 函式進行回傳。
而 user 端就只要做跟 server 端相同的事,只是改為先傳輸,再等回傳。
奇怪的是,我原本以為程式執行到 recvfrom 就會卡住,收到資料後往下執行,然後迴圈再執行回 recvfrom 等待,因此我原本想法是如果 server 正在執行資料處理,那這時候其他用戶傳資料過來應該沒辦法收到,因此我做了以下實驗,先開啟 1 個 client,當 server 接收到資料後,先 sleep 10 秒鐘,而我就在這 10 秒鐘再開啟一個 client 端的程式傳輸資料,我預期應該只有第一個 client 會接收到資料,但實際上兩個 client 都有收到回覆
此外,我並沒有設定 client 端的資料,client 也沒有 bind 到某個 port,卻能成功執行?
檢查後發現是回傳到 127.0.0.1:41173
,port 每次不同,這是代表原本就要 bind 到某個 port 嗎?
假設我有先進行綁定:
這次就成功抓到 port 54321 的資料,因此似乎在創建 socket 時,除非有綁定,否則就會自動分配一個 port 給他?
相比 UDP , TCP 必須建立連線,因此在 server 端要先透過 listen 進行監聽 (聽有沒有人要建立連線)
backlog
代表最大連線數
在 client 端,則要使用 connect 進行連接,代連線建立完成就會被放到 complete connection queue。
接下來使用 access
就可以獲取 complete connection queue 的資料(所以會被處理的資料都是已經建好連線了)
https://hackmd.io/@fatCatOrange/ktcp
關於 RCU 的筆記:https://hackmd.io/FxDhzUmKRnKKBl1RSFkpsg
ktime
stack 使用量?
1.2.4
MRE: https://en.wikipedia.org/wiki/Minimal_reproducible_example