[南大ICS-PA2] 字符串处理函数和printf实现

[南大ICS-PA2] 程序、运行时环境与AM sting printf实现

  • AM-裸机(bare-metal)运行时环境
  • RTFSC(3)
    • 通过批处理模式运行NEMU
  • 实现常用的函数
    • 实现字符串处理函数
      • `size_t strlen(const char *s);`
      • `char *strcpy(char *dst, const char *src);`
      • `char *strncpy(char *dst, const char *src, size_t n);`
      • `char *strcat(char *dst, const char *src);`
      • `int strcmp(const char *s1, const char *s2) ;`
      • `int strncmp(const char *s1, const char *s2, size_t n) ;`
      • `void *memset(void *s, int c, size_t n);`
      • `void *memmove(void *dst, const void *src, size_t n);`
      • `void *memcpy(void *out, const void *in, size_t n);`
      • `int memcmp(const void *s1, const void *s2, size_t n);`
    • 实现`sprintf`
      • stdarg是如何实现的?

AM-裸机(bare-metal)运行时环境

AM(Abstract Machine),作为一个向程序提供运行时环境的库,AM根据程序的需求把库划分成以下模块:
A M = T R M + I O E + C T E + V M E + M P E \rm AM = TRM + IOE + CTE + VME + MPE AM=TRM+IOE+CTE+VME+MPE

  • TRM(Turing Machine) - 图灵机,最简单得运行时环境,为程序提供基本得计算能力
  • IOE(I/O Extension) - 输入输出扩展, 为程序提供输入输出的能力
  • CTE(Context Extension) - 上下文扩展,为程序提供上下文管理能力
  • VME(Virtual Memory Extension) - 虚存扩展,为程序提供虚拟内存管理能力
  • MPE(Multi-Processor Extension) - 多处理器扩展,为程序提供多处理器通信的能力

RTFSC(3)

  • am:不同架构得AM API实现,目前我们只需要关注NEMU相关得内容即可。am.h中列出了AM中的所有API。
  • klib:一些架构无关的库函数,方便应用程序开发

通过批处理模式运行NEMU

我们知道, 大部分同学很可能会这么想: 反正我不阅读Makefile, 老师助教也不知道, 总觉得不看也无所谓.

所以在这里我们加一道必做题: 我们之前启动NEMU的时候, 每次都需要手动键入c才能运行客户程序. 但如果不是为了使用NEMU中的sdb, 我们其实可以节省c的键入. NEMU中实现了一个批处理模式, 可以在启动NEMU之后直接运行客户程序. 请你阅读NEMU的代码并合适地修改Makefile, 使得通过AM的Makefile可以默认启动批处理模式的NEMU.

你现在仍然可以跳过这道必做题, 但很快你就会感到不那么方便了.

答:在nemu的nemu-main.c中,main函数如下:

int main(int argc, char *argv[]) {/* Initialize the monitor. */
#ifdef CONFIG_TARGET_AMam_init_monitor();
#elseinit_monitor(argc, argv);
#endif/* Start engine. */engine_start();return is_exit_status_bad();
}

可以看出,当定义了CONFIG_TARGET_AM后,不会出现sdb的提示

不用修改makefile,初始化时使用批处理模式即可

void init_sdb() {/* Compile the regular expressions. */init_regex();/* Initialize the watchpoint pool. */init_wp_pool();sdb_set_batch_mode();
}

实现常用的函数

  • AM:架构相关的运行时环境
  • klib:架构无关的库函数,kernel library

实现字符串处理函数

根据需要实现abstract-machine/klib/src/string.c中列出的字符串处理函数, 让cpu-tests中的测试用例string可以成功运行. 关于这些库函数的具体行为, 请务必RTFM.

size_t strlen(const char *s);
char *strcpy(char *dst, const char *src);
char *strncpy(char *dst, const char *src, size_t n);
char *strcat(char *dst, const char *src)int strcmp(const char *s1, const char *s2)int strncmp(const char *s1, const char *s2, size_t n) ;
void *memset(void *s, int c, size_t n);
void *memmove(void *dst, const void *src, size_t n);
void *memcpy(void *out, const void *in, size_t n);
int memcmp(const void *s1, const void *s2, size_t n);

size_t strlen(const char *s);

  • 返回以NULL终止得字节字符串得长度,即字符数组中的字符数,其第一个元素用str指向,直到但不包括第一个空字符
  • 参数s是空指针,函数返回0.
size_t strlen(const char *s) {if (s == NULL) {return 0;}size_t n = 0;while(s[n] != '\0') {++n;}return n;
}

char *strcpy(char *dst, const char *src);

  • 将 src 指向的空终止字节字符串(包括空终止符)复制到其第一个元素由 dest 指向的字符数组。

  • 当dest数组长度不够时,行为未定义。两个string重叠的行为也未定义。如果 dest 不是指向字符数组的指针或 src 不是指向空终止字节字符串的指针,则行为未定义。

    注:实现时直接复制,未定义行为有操作系统处理

char *strcpy(char *dst, const char *src) {if (src == NULL || dst == NULL) {   // 没有所指,直接返回dstreturn dst;}// 当成指向字符数组处理,所以即使没有空字符,导致内存访问越界,或修改了其他有用的数据也不管,因为这是函数调用者所需要保证的,下面一些string函数都是这样对带非字符串数组char *res = dst;do {*dst = *src;dst++;src++;} while(*src != '\0');  return res;
}

char *strncpy(char *dst, const char *src, size_t n);

  • 将 src 指向的字符数组中至多 n 个字符(包括终止空字符,但不包括空字符之后的任何字符)复制到 dest 指向的字符数组。
  • 如果在复制整个数组 src 之前达到 n,则生成的字符数组不是以 null 结尾的。
  • 如果从 src 复制终止空字符后,未达到 n,则将额外的空字符写入 dest,直到写入了 n 个字符的总数。
  • 如果字符数组重叠,如果 dest 或 src 不是指向字符数组的指针(包括如果 dest 或 src 是空指针),如果 dest 指向的数组的大小小于 n,或者如果 src 指向的数组的大小小于 n 并且它不包含空字符,则行为未定义。
char *strncpy(char *dst, const char *src, size_t n) {if (src == NULL || dst == NULL) {return dst;}char *ans = dst;while (*src != '\0' && n != 0) {*dst = *src;++dst;++src;--n;}// 将额外的空字符写入dest,直到写入了n个字符的总数。while (n != 0) {*dst = '\0';++dst;--n;}return ans;
}

char *strcat(char *dst, const char *src);

  • 将 src 指向的空终止字节字符串的副本附加到 dest 指向的空终止字节字符串的末尾。 字符 src[0] 替换 dest 末尾的空终止符。 生成的字节字符串以 null 结尾。
  • 如果目标数组不足以容纳 src 和 dest 的内容以及终止空字符,则行为未定义。 如果字符串重叠,则行为未定义。 如果 dest 或 src 不是指向空终止字节字符串的指针,则行为未定义。
char *strcat(char *dst, const char *src) {if (src == NULL || dst == NULL) {return dst;}char *ans = dst;while (*dst != '\0') {++dst;}do {*dst = *src;dst++;src++;} while(*src != '\0');  // 先做一次,可以保证将结束字符也复制进去return ans;
}

int strcmp(const char *s1, const char *s2) ;

  • 按字典顺序比较两个以 null 结尾的字节字符串。
  • 结果的符号是被比较字符串中不同的第一对字符(均解释为 unsigned char)的值之间的差异符号。
  • 如果 lhs 或 rhs 不是指向空终止字节字符串的指针,则行为未定义。
int strcmp(const char *s1, const char *s2) {if (s1 == NULL || s2 == NULL) {return 0;}while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) {s1++;s2++;}return *s1 == *s2 ? 0 : (unsigned char)*s1 < (unsigned char)*s2 ? -1 : 1;
}

int strncmp(const char *s1, const char *s2, size_t n) ;

  • 最多比较两个可能以 null 结尾的数组的 n 个字符。 比较是按字典顺序进行的。不比较空字符后面的字符。
  • 结果的符号是被比较数组中不同的第一对字符(均解释为 unsigned char)的值之间的差异符号。
  • 当访问发生在数组 lhs 或 rhs 末尾之后时,行为未定义。 当 lhs 或 rhs 是空指针时,行为未定义。
int strncmp(const char *s1, const char *s2, size_t n) {if (s1 == NULL || s2 == NULL) {return 0;}while (n != 0 && *s1 != '\0' && *s2 != '\0' && *s1 == *s2) {--n;++s1;++s2;}// 当比较了n次后,即新n变为0时,此时两个字符串也是相等得,memcmp同理return *s1 == *s2 || n == 0 ? 0 : (unsigned char)*s1 < (unsigned char)*s2 ? -1 : 1;
}

void *memset(void *s, int c, size_t n);

  • 将值 (unsigned char)c 复制到 s 指向的对象的每个前 n 个字符中。
  • 如果访问发生在 dest 数组末尾之外,则行为未定义。 如果 dest 是空指针,则行为未定义。
void *memset(void *s, int c, size_t n) {if (s == NULL) {return s;}unsigned char *src = s;   // 先讲传入得指针,做无符号字符解释while (n != 0) {--n;*src = c;++src;}return s;
}

void *memmove(void *dst, const void *src, size_t n);

  • 将 n 个字符从 src 指向的对象复制到 dest 指向的对象。 这两个对象都被解释为 unsigned char 数组。
  • **对象可能重叠:**复制就像将字符复制到临时字符数组,然后将字符从数组复制到目标一样。
  • 如果访问发生在 dest 数组末尾之外,则行为未定义。
  • 如果 dest 或 src 是无效指针或空指针,则行为未定义。

memmove 可用于设置分配函数获得的对象的有效类型。

尽管被指定为“好像”使用了临时缓冲区,但此函数的实际实现不会产生开销或双重复制或额外的内存。 一种常见的方法(glibc 和 bsd libc)是如果目标在源之前开始,则从缓冲区的开头向前复制字节,否则从结尾向后复制,当没有重叠时回退到更高效的 memcpy 全部。

在严格别名禁止将同一内存检查为两种不同类型的值的情况下,可以使用 memmove 来转换这些值。

void *memmove(void *dst, const void *src, size_t n) {if (dst == NULL || src == NULL || n == 0 || dst == src) {return dst;}unsigned char *dest = dst;const unsigned char *source = src;if (dst < src) {while (n != 0) {--n;*dest = *source;++dest;++source;}} else {dest += n;source += n;while (n != 0) {--n;--dest;--source;*dest = *source;}}return dst;
}

void *memcpy(void *out, const void *in, size_t n);

  • 将 n 个字符从 src 指向的对象复制到 dest 指向的对象。 这两个对象都被解释为 unsigned char 数组。
  • 如果访问发生在 dest 数组末尾之外,则行为未定义。 如果对象重叠(这违反了限制契约)(自 C99 起),则行为未定义。 如果 dest 或 src 是无效指针或空指针,则行为未定义。
void *memcpy(void *out, const void *in, size_t n) {if (out == NULL || in == NULL || n == 0 || out == in) {return out;}unsigned char *dest = out;const unsigned char *src = in;while (n != 0) {*dest = *src;--n;++dest;++src;}return out;
}

int memcmp(const void *s1, const void *s2, size_t n);

  • 比较 lhs 和 rhs 指向的对象的前 count 个字节。 比较是按字典顺序进行的。
  • 结果的符号是被比较对象中不同的第一对字节(均解释为 unsigned char)的值之间差异的符号。
  • 如果访问发生在 lhs 和 rhs 指向的任一对象的末尾之外,则行为未定义。 如果 lhs 或 rhs 是空指针,则行为未定义。
int memcmp(const void *s1, const void *s2, size_t n) {if (s1 == NULL || s2 == NULL) {return 0;}const unsigned char *src1 = s1;const unsigned char *src2 = s2;while (n != 0 && *src1 != '\0' && *src2 != '\0' && *src1 == *src2) {--n;++src1;++src2;}return *src1 == *src2 || n == 0 ? 0 : *src1 < *src2 ? -1 : 1;
}

实现sprintf

实现abstract-machine/klib/src/stdio.c中的sprintf(), 具体行为可以参考man 3 printf. 目前你只需要实现%s%d就能通过hello-str的测试了, 其它功能(包括位宽, 精度等)可以在将来需要的时候再自行实现.

printf()+sprintf()源码解析笔记

#include 
#include 
#include 
#include 
#if !defined(__ISA_NATIVE__) || defined(__NATIVE_USE_KLIB__)
#define ZEROPAD	1		/* pad with zero 填补0*/
#define SIGN	2		/* unsigned/signed long */
#define PLUS	4		/* show plus 显示+*/
#define SPACE	8		/* space if plus 加上空格*/
#define LEFT	16		/* left justified 左对齐*/
#define SPECIAL	32		/* 0x /0*/
#define LARGE	64		/* 用 'ABCDEF'/'abcdef' */
uint32_t __attribute__((weak)) __div64_32(uint64_t *n, uint32_t base)
{uint64_t rem = *n;uint64_t b = base;uint64_t res, d = 1;uint32_t high = rem >> 32;/* Reduce the thing a bit first */res = 0;if (high >= base) {high /= base;res = (uint64_t) high << 32;rem -= (uint64_t) (high*base) << 32;}while ((int64_t)b > 0 && b < rem) {b = b+b;d = d+d;}do {if (rem >= b) {rem -= b;res += d;}b >>= 1;d >>= 1;} while (d);*n = res;return rem;
}//进制之间的相应转换
# define do_div(n,base) ({						\unsigned int __base = (base);					\unsigned int __rem;	\(void)(((typeof((n)) *)0) == ((uint64_t *)0));	/*这一句的作用是为了消去警告,因为定义了n变量而没有使用到它,会报警*/ \if (((n) >> 32) == 0) {						/*32位4字节*/ \__rem = (unsigned int)(n) % __base;			/*对应的base进制位*/ \(n) = (unsigned int)(n) / __base;			\} else								\__rem = __div64_32(&(n), __base);			/*64转32*/ \__rem;								\})//以特定的进制格式化输出字符
static char * number(char * str, unsigned long long num, int base, int size, int precision, int type)
{char c,sign,tmp[66];const char *digits="0123456789abcdefghijklmnopqrstuvwxyz";int i;if (type & LARGE)//输出大写字符,例如十六进制0XFF112233AAdigits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";if (type & LEFT)//如果有'-',如果出现了左对齐,就取消前面补0type &= ~ZEROPAD;if (base < 2 || base > 36)return 0;c = (type & ZEROPAD) ? '0' : ' ';//如果标志符有0则补0,否则补空格;例如%02dsign = 0;//符号if (type & SIGN) //有符号与无符号的转换{if ((signed long long)num < 0) {sign = '-';num = - (signed long long)num;//取正值size--;//字段宽度减1} else if (type & PLUS) //显示+{sign = '+';size--;} else if (type & SPACE)//填补空格{sign = ' ';size--;}}//处理十六进制字宽问题if (type & SPECIAL) //十六进制显示{if (base == 16)size -= 2;//0xelse if (base == 8)size--;//0}i = 0;if (num == 0)//如果参数为0,则记录字符0tmp[i++]='0'; //tmp中的内容会放到缓冲区中else while (num != 0) //循环,num /= base{// tmp[i++] = digits[do_div(num, base)];//将进制转换,低地址先进tmp?// FIXME 这里只考虑了十进制tmp[i++] = digits[num % base];num /= base;}//地址长度大于精度,直接按地址长度输出,如果精度大于地址位数,先补空格//例如:printf("%18p\n",&a);-->空格空格00000000FAF27284if (i > precision)precision = i;size -= precision;if (!(type&(ZEROPAD+LEFT)))//没有'-'和补0,直接补空格while(size-->0)*str++ = ' ';if (sign)//如果有符号,输出符号,符号包括:'-','+','',0*str++ = sign;if (type & SPECIAL) //输出8进制或16进制符号0或0x{if (base==8)*str++ = '0';else if (base==16) {*str++ = '0';*str++ = digits[33];//x或X}}if (!(type & LEFT))//没有-while (size-- > 0)*str++ = c;//c为0或空格while (i < precision--)//i为转换后存在tmp中字符的个数*str++ = '0';while (i-- > 0)*str++ = tmp[i];//tmp中存储着转换了的参数while (size-- > 0)*str++ = ' ';return str;
}//获得字段转化为整数,例如%12d中的字母12提出来变成整型12.
static int skip_atoi(const char **s)//二级指针,存进来的是字符串的地址
{int i, c;for (i = 0; '0' <= (c = **s) && c <= '9'; ++*s)i = i*10 + c - '0';return i;
}static char sprint_buf[1024];
/*可变函数在内部实现的过程中是从右向左压入堆栈,从而保证了可变参数的第一个参数始终位于栈顶*/
int printf(const char *fmt, ...)//可以有一个或多个固定参数
{va_list args; //用于存放参数列表的数据结构int n;/*根据最后一个fmt来初始化参数列表,至于为什么是最后一个参数,是与va_start有关,感兴趣的朋友可以先去了解一下变参函数和里面用到的相关宏的作用。*/va_start(args, fmt);n = vsprintf(sprint_buf, fmt, args);va_end(args);//执行清理参数列表的工作// if (console_ops.write)// 		console_ops.write(sprint_buf, n);return n;
}int vsprintf(char *buf, const char *fmt, va_list args) {int len;unsigned long long num;int i, base;char * str;const char *s;int flags;		/* 用在number()函数的标志 */int field_width;	/* 输出字段的宽度 *///精度;用在浮点数时表示输出小数点后几位;用在字符串时表示输出字符个数 int precision;		int qualifier;		/* 'h', 'l', or 'L' for integer fields *//* 'z' support added 23/7/1999 S.H.    *//* 'z' changed to 'Z' --davidm 1/25/99 *//*将字符逐个放到输出缓冲区中,直到遇到第一个%*/for (str=buf ; *fmt ; ++fmt) {if (*fmt != '%') {    //寻找%*str++ = *fmt;continue;}//遇到%后执行下面代码	/* process flags */flags = 0;
repeat:++fmt;				//跳过第一个 '%'switch (*fmt) 		//判断%后面的字符,对格式运算符的标志的处理{		case '-': flags |= LEFT; goto repeat;//flags=10000(二进制,下面一样)case '+': flags |= PLUS; goto repeat;//flags=100case ' ': flags |= SPACE; goto repeat;//flags=1000case '#': flags |= SPECIAL; goto repeat;//flags=10 0000case '0': flags |= ZEROPAD; goto repeat;//flags=1}//对字段宽度的处理field_width = -1;if ('0' <= *fmt && *fmt <= '9')field_width = skip_atoi(&fmt);  //得到字段宽度else if (*fmt == '*')               //*表示可变宽度{             ++fmt;field_width = va_arg(args, int);//获得表示字段宽度的参数,/*一般使用最后一个固定参数args来初始化这个函数,得到的下一个参数为第一个变参,即printf("%*d",a,b);中的a,这里a表示字段宽度。字符串为固定参数;变参函数至少要有一个固定参数。*/if (field_width < 0) //手动输入负数,左对齐,例如printf("%*d",-2,3);{	field_width = -field_width;flags |= LEFT;}}// 获取精度 precision = -1;if (*fmt == '.') {++fmt;	if ('0' <= *fmt && *fmt <= '9')precision = skip_atoi(&fmt);//获得精度else if (*fmt == '*') //可变精度,printf("%.*f",2,3.1415);-->3.14{++fmt;/* 获取表示精度的参数(以整数类型获取) */precision = va_arg(args, int);}if (precision < 0)//精度不能小于0precision = 0;}//获取转换修饰符,即%hd、%ld、%lld、%Lf...中的h、l、L、Z (ll用q代替)qualifier = -1;if (*fmt == 'l' && *(fmt + 1) == 'l') {qualifier = 'q';//即llfmt += 2;} else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'|| *fmt == 'Z') {qualifier = *fmt;++fmt;}base = 10;//默认十进制//对c、s、p、n、%、o等做处理switch (*fmt) {//转换格式符为%ccase 'c'://如果没有有‘-’,先输出字宽-1个空格再输出字符if (!(flags & LEFT))//如果没有'-'标记符while (--field_width > 0)*str++ = ' ';//根据字段输出空格' '/*获取字符参数时是先以int类型获取再强转为unsigned char,为了获取过程中保证精度不丢失。*/*str++ = (unsigned char) va_arg(args, int);// 如果有'-',先输出字符再填补空格,注意是先--的,所以实际空格会比输入的字段少1,在加					上参数就刚好够宽度;比如printf("%5d",2);输出:空格空格空格空格2。while (--field_width > 0)*str++ = ' ';continue;//转换格式符为%s           case 's':s = va_arg(args, char *);//char*格式获取参数if (!s)                  //如果字符串不存在,则返回(NULL)s = "";/*如果字符串中字符个数大于精度,len为精度;否则len为字符个数,即精度表示了字符串输出字符的个数*/len = strnlen(s, precision);//处理'-',即printf("%-s","hello");if (!(flags & LEFT))while (len < field_width--)*str++ = ' ';for (i = 0; i < len; ++i)*str++ = *s++;while (len < field_width--)*str++ = ' ';continue;//处理格式符%p       case 'p':if (field_width == -1) //如果没有设置字段宽度{ /*字宽为8或16(根据系统而定),因为2个位表示一个直接;例如32位系统指针大小位4字节,oxFF FF FF FF,需要8个字宽才能存储*/field_width = 2*sizeof(void *);flags |= ZEROPAD;   //flags = 1;会在前面补0}//转为16进制并存进缓冲区中str = number(str, (unsigned long) va_arg(args, void *), 16,field_width, precision, flags);continue;//buf为1024字节空间的输出缓冲区(静态char数组)case 'n':if (qualifier == 'l') {long * ip = va_arg(args, long *);*ip = (str - buf);//获取输出缓冲数组中的个数} else if (qualifier == 'Z') {size_t * ip = va_arg(args, size_t *);*ip = (str - buf);} else {int * ip = va_arg(args, int *);*ip = (str - buf);}continue;case '%':*str++ = '%';continue;/* integer number formats - set up the flags and "break" */case 'o':base = 8;break;case 'X':flags |= LARGE;//小写转大写case 'x':  //十六进制base = 16;break;case 'd':	//十进制case 'i':flags |= SIGN;case 'u':	//无符号break;default:*str++ = '%';if (*fmt)*str++ = *fmt;else--fmt;continue;}if (qualifier == 'l') {num = va_arg(args, unsigned long);if (flags & SIGN)num = (signed long) num;} else if (qualifier == 'q') {num = va_arg(args, unsigned long long);if (flags & SIGN)num = (signed long long) num;} else if (qualifier == 'Z') {num = va_arg(args, size_t);} else if (qualifier == 'h') {//输出短整型时是先以整数来获取参数,再转为短整型输出,保证获取过程中精度不丢失num = (unsigned short) va_arg(args, int);if (flags & SIGN)num = (signed short) num;} else {//没有特殊标志的格式符,一律先以无符号整型获取,再转为有符号整型num = va_arg(args, unsigned int);if (flags & SIGN)num = (signed int) num;}//转换为对应的个数再存到缓冲区中str = number(str, num, base, field_width, precision, flags);}*str = '\0';//最后以'\0'结束return str-buf;
}int sprintf(char *out, const char *fmt, ...) {va_list args;int i;va_start(args, fmt);i = vsprintf(out, fmt, args);va_end(args);return i; 
}int snprintf(char *out, size_t n, const char *fmt, ...) {panic("Not implemented");
}int vsnprintf(char *out, size_t n, const char *fmt, va_list ap) {panic("Not implemented");
}#undef ZEROPAD	/* pad with zero 填补0*/
#undef SIGN			/* unsigned/signed long */
#undef PLUS			/* show plus 显示+*/
#undef SPACE	  /* space if plus 加上空格*/
#undef LEFT			/* left justified 左对齐*/
#undef SPECIAL	/* 0x /0*/
#undef LARGE		/* 用 'ABCDEF'/'abcdef' */#undef do_div#endif

stdarg是如何实现的?

stdarg.h中包含一些获取函数调用参数的宏, 它们可以看做是调用约定中关于参数传递方式的抽象. 不同ISA的ABI规范会定义不同的函数参数传递方式, 如果让你来实现这些宏, 你会如何实现?


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部