ysyx: 完善庫函式

namezhyp發表於2024-08-01

  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下的函式實現似乎也有所不同。

相關文章