nemu把庫函式分為了與架構有關的isa部分和與架構無關的klib部分。這部分的任務,就是完善stdio.c stdlib.c 和string.c,讓各種測試集、跑分和demo可以正常執行。值得一提的是,我也是看到這一部分,回看測試集時,才注意到測試集用的其實都是C語言自帶的關鍵字和基本功能,沒有使用到特定庫函式。在PA2初期,只需要完善少數庫函式就可以讓hello-str string測試集正常執行,在PA2後期,為了執行demo和更多測試。剩下的函式應該按照講義儘量都實現。
庫函式的實現方面,hello-str的程式碼如下:
#include "trap.h" char buf[128]; int main() { sprintf(buf, "%s", "Hello world!\n"); check(strcmp(buf, "Hello world!\n") == 0); sprintf(buf, "%d + %d = %d\n", 1, 1, 2); check(strcmp(buf, "1 + 1 = 2\n") == 0); sprintf(buf, "%d + %d = %d\n", 2, 10, 12); check(strcmp(buf, "2 + 10 = 12\n") == 0); return 0; }
可以很清楚的看到,我們需要實現的是sprintf strcmp兩個函式,考慮到字串需要最基本的長度資訊,可以考慮用kilb-marco.h裡面的LENGTH宏,也可以先實現strlen函式。 我選擇先實現strlen函式,然後用strlen完成strcmp。strcmp的實現思路是:不用考慮兩個字串長度相等/不相等分情況,我們只需要直接找出二者中長度較小的值,以此值進行遍歷即可。如果字串在非結尾處就不一樣,那麼返回值就是str1的對應項減去str2的對應項。如果二者不一樣長,其中一個字串又是另一個字串的子串,那麼直接用\0減去下一個字元就行了:
for(i=0; i<min; i++) { if(s1[i] != s2[i]) { return (unsigned char)s1[i] - (unsigned char)s2[i]; } } return (unsigned char)s1[min] - (unsigned char)s2[min];
sprintf的實現則更有意思一些,掃描到百分號後根據下一字元做switch即可。目前暫時不考慮更多輸出格式和長度格式。printf在後面實現時,基本思路也是一樣的,只不過需要putch()向緩衝區輸出字元。
----------------------
在後面想要執行程式碼雨等demo時,需要實現malloc函式。在這裡我們只需要實現一個簡單的malloc功能,但儘管如此,地址對齊也還是要考慮的。可以參考microbench裡面的bench.c,它的邏輯基本可以套用在這裡。
bench.c裡alloc()的實現:
void* bench_alloc(size_t size) { size = (size_t)ROUNDUP(size, 8); char *old = hbrk; hbrk += size; assert((uintptr_t)heap.start <= (uintptr_t)hbrk && (uintptr_t)hbrk < (uintptr_t)heap.end); for (uint64_t *p = (uint64_t *)old; p != (uint64_t *)hbrk; p ++) { *p = 0; } assert((uintptr_t)hbrk - (uintptr_t)heap.start <= setting->mlim); return old; }
bech_alloc使用了ROUNDUP這樣一個宏,展開以後是((((uintptr_t)size) + (8) - 1) & ~((8) - 1)) ,它的功能就是做記憶體對齊,後面的8-1為7,按位取反後,後三位是000,做到了對齊的效果。在我們的malloc裡同樣可以用這個宏。只不過在用的時候有個小問題:bench這裡為什麼要用8?這不是64位機才需要的值嗎?
同時,在我們的klib裡,addr的初始值需要被設定為heap.start,表示堆的起始地址。後續每次malloc都會更新。
----------------------------------------------
想要執行bad apple,還需要實現memmove等函式。memcpy和memmove兩個函式的不同之處,就在於memmove更加安全,它會考慮到源地址和目的地址存在重疊的情況。
在源記憶體和目的記憶體不重疊的情況下,memmove和memcpy的行為可以是一樣的,由於多了判斷部分,可能速度會稍微慢一些。在發生重疊時,memcpy就可能出現無法正確複製的情況:
以上面這張圖為例,源記憶體塊在前,目的記憶體塊在後,並且存在重疊。如果還是從前往後複製的話,src記憶體塊就會從重疊部分開始被破壞,導致複製錯誤。面對這種情況,正確做法就是從後往前的順序逐個複製記憶體。
同樣的,面對這種情況:
此時就應該使用從前往後的順序逐個複製,從後往前會破壞記憶體。
memmove的部分實現:
unsigned char *d = (unsigned char *)dst; const unsigned char *s = (const unsigned char *)src; // 目的地址在前,且記憶體區域重疊 從前往後複製 if (d < s && d+n > s) { for(size_t i = 0; i < n; i++) { d[i] = s[i]; } } // 目標地址在後,且記憶體區域重疊 從後往前複製 else if (d>s && s+n > d) { for(size_t i = n; i > 0; i--) { d[i-1] = s[i-1]; } } // 不重疊 else { for(size_t i = 0; i < n; i++) { d[i] = s[i]; } }
別忘了void* 指標要正確++,需要先強制轉換為char *指標。
參考:【C語言】memmove()函式詳解(複製重疊記憶體塊函式)-CSDN部落格
-----------------------
關於庫函式的實現,標準庫函式其實也可能存在漏洞,或者說故意存在漏洞。比如vs下的strncpy函式,它的原型是這樣實現的:
char * __cdecl strncpy ( char * dest, const char * source, size_t count ) { char *start = dest; while (count && (*dest++ = *source++) != '\0') /* copy string */ count--; if (count) /* pad out with zeroes */ while (--count) *dest++ = '\0'; return(start); }
這個實現有個隱藏問題:輸入引數可能是指標,也可能是字元陣列。如果將長字元陣列強行復制入短字元陣列,就有可能出現問題。
在實現自己的klib時,要採用更安全的實現,還是依照原樣由自己決定,因為哪怕函式存在漏洞,人在呼叫時本身根據用法也應該儘量避免掉。此外,linux下的函式實現似乎也有所不同。