ysyx: 完善库函数

  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下的函数实现似乎也有所不同。

posted @ 2024-08-01 15:54  namezhyp  阅读(58)  评论(0编辑  收藏  举报