在前面编写简单的c运行库(一)中主要实现了调用main函数前的初始化、获取参数和环境变量、退出程序等工作。接下来我们真正实现c标准库中的一些函数(主要是文件操作、字符串操作函数)。不过我们对这些函数的实现力争简单,对于效率方面考虑的不是很多,因为目的主要还是学习神秘的库是怎么实现的。
1 文件操作
c中的标准I/O库都是带有缓存的,我们在这里为了实现的简单,将缓存省略了,直接包装了有关文件操作的系统调用。现在我们直接看文件打开的函数:
1 static int open(const char *pathname, int flags, int mode)
2 {
3 int ret;
4
5 __asm__ volatile(
6 "int $0x80"
7 :"=a"(ret)
8 :"0"(5),"b"(pathname),"c"(flags),"d"(mode)
9 );
10 if (ret >= 0)
11 return ret;
12 return -1;
13 }
open函数中直接调用了嵌入汇编调用了系统调用。对于系统调用的返回值,如果是负数,直接返回-1,否则直接返回。这个函数是系统调用的一个包装,本质其实就是个系统调用。然后我们在open函数的基础上实现c标志库函数中的fopen函数。
1 FILE *fopen(const char *path, const char *mode)
2 {
3 int fd = -1;
4 int flags = 0;
5 int access = 00700; /*创建文件的权限*/
6
7 if (strcmp(mode, "w") == 0)
8 flags |= O_WRONLY | O_CREAT | O_TRUNC;
9 if (strcmp(mode, "w+") == 0)
10 flags |= O_RDWR | O_CREAT | O_TRUNC;
11 if (strcmp(mode, "r") == 0)
12 flags |= O_RDONLY;
13 if (strcmp(mode, "r+") == 0)
14 flags |= O_RDWR | O_CREAT;
15 fd = open(path, flags, access);
16 return (FILE *)fd;
17 }
由于我没有像标志I/O库那样实现缓存,所以我直接把FILE定义为int型,这样我们用FILE就相当于用了文件描述符。从上面的代码中可以知道我设置了文件的创建权限只有文件创建者有读写执行的权限,还有就是我只实现了以只读、只写、读写方式打开文件,对于追加等方式没有实现。然后函数read、fread和write、fwrite都可以用相同的方式实现,还有fputc,fputs也是已一样的。
2 输出函数
I/O函数中比较麻烦的要属实现printf、fprintf这些可变参数的函数,当然这些函数都是调用vfprintf函数实现的,所以只要实现了vfprintf函数,其它的函数实现就比较简单了。
首先来看下我实现的vfprintf函数代码:
1 int vfprintf(FILE *stream, const char *format, va_list ap)
2 {
3 int n = 0, flag = 0, ret;
4 char str[20];
5
6 while (*format)
7 {
8 switch (*format)
9 {
10 case '%':
11 if (flag == 1)
12 {
13 fputc('%', stream);
14 flag = 0;
15 n ++;
16 }
17 else
18 flag = 1;
19 break;
20 case 'd':
21 if (flag == 1)
22 {
23 itoa((int)va_arg(ap, int), str, 10);
24 ret = fputs(str, stream);
25 n += ret;
26 }
27 else
28 {
29 fputc('d', stream);
30 n ++;
31 }
32 flag = 0;
33 break;
34 case 's':
35 if (flag == 1)
36 {
37 ret = fputs((char *)va_arg(ap, char *), stream);
38 n += ret;
39 }
40 else
41 {
42 fputc('s', stream);
43 n ++;
44 }
45 flag = 0;
46 break;
47 case '\n':
48 /*换行*/
49 fputc(0x0d, stream);
50 n ++;
51 fputc(0x0a, stream);
52 n ++;
53 break;
54 default:
55 fputc(*format, stream);
56 n ++;
57 }
58 format ++;
59 }
60 return n;
61 }
vfprintf主要麻烦的是对格式化字符串的分析,我们在这里使用一种比较简单的算法:
(1)定义模式:翻译模式/普通模式
(2)循环整个格式字符串
a) 如果遇到%
i 普通模式:进入翻译模式
ii 翻译模式: 输出%, 退出翻译模式
b) 如果遇到%后面允许出现的特殊字符(如d和s)
i 翻译模式:从不定参数中取出一个参数输出,退出翻译模式
ii 普通模式:直接输出该字符串
c) 如果遇到其它字符(除\n):无条件退出翻译模式并输出字符
d) 如果遇到'\n'字符,如果直接输出是不能达到换行的效果的,必须要同时输出回车换行才行
从上面的实现vfprintf的代码中可以看出,并不支持特殊的格式控制符,例如位数、进度控制等,仅支持%d与%s这样的简单转换。真正的vfprintf格式化字符串实现比较复杂,因为它支持诸如“%f”、“%x”已有的各种格式、位数、精度控制等。我觉得上面实现的代码已经充分的展示了vfprintf的实现原理和它的关键技巧,所以没有必要一个一个的都实现。现在来实现printf的就简单多了,下面是printf的实现代码:
1 int printf(const char *format, ...)
2 {
3 int n;
4 va_list ap;
5
6 va_start(ap, format);
7 n = vfprintf(stdout, format, ap);
8 va_end(ap);
9 return n;
10 }
对于可变参数的编程,我已经在c语言中的可变参数编程中详细的讲过了,包括它的实现原理。所以只要了解了可变参数的编程,对于实现printf函数来说就真的没什么难度了,纯粹就是调用vfprintf函数而已。如果实现了printf函数,那么对于实现scanf、fscanf也是同样的原理。
在编写简单的c运行库(二)中主要实现了对有关文件操作函数的实现,接下来主要实现有关字符串的函数,如itoa,strcmp,strcpy,strlen函数,这些函数并没有用到系统调用,所以也就不用向实现文件操作的函数那样使用内嵌汇编,这些函数的定义都放在string.h中。实现了字符串函数之后,就大概实现了一个小型的c运行库,虽然很简略,但对于理解c库函数运行原理、所用的关键技术有了比较深刻的认识。最后用这个小的c运行库来编译运行一个简单的测试程序,用以测试我们的库能否正常的工作。
1 字符串函数
字符串函数中主要是实现itoa函数有点难度,其它的都还比较的简单,所以这里主要讲下itoa函数的实现。
1 char *itoa(int n, char *str, int radix)
2 {
3 char digit[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
4 char *ptr = str, *base;
5
6 if (!str || radix < 2 || radix > 36)
7 return str;
8 if (radix != 10 && n < 0)
9 return str;
10 if (!n)
11 {
12 *ptr++ = '0';
13 *ptr = 0;
14 }
15 if (radix == 10 && n < 0)
16 {
17 *ptr++ = '-';
18 n = -n;
19 }
20 base = ptr;
21 while (n)
22 {
23 *ptr++ = digit[n % radix];
24 n /= radix;
25 }
26 *ptr = 0;
27 for (-- ptr; base < ptr; base ++, ptr --)
28 {
29 *ptr ^= *base;
30 *base ^= *ptr;
31 *ptr ^= *base;
32 }
33 return str;
34 }
itoa函数功能是把一个整数转换为字符串,我们在编写前面vfprintf函数的时候其实就已经用到过,它在c编程中也是经常用到的。从上面的代码中可以看到itoa支持2-36进制的整数转换为字符串。在这个函数中只认为十进制的数才能带有"-"号,所以在代码的第15行判断该整数是否满足是十进制的负数,如果满足在数的最前面加个"-"号,其它进制的负数默认不带"-"号。21-25行根据数的进制把数的低位到高位一个一个的分离并保存到ptr字符数组中,但是输出字符串中高位应该放在前面,所以27-32行主要是对ptr字符数组做一个倒置操作。
2 测试库
接下来用一个简单的程序来测试编写的运行库,测试程序如下:
1 #include "minicrt.h"
2
3
4 extern char **environ;
5
6 int main ( int argc, char *argv[] )
7 {
8 int i;
9 FILE *fp;
10 char **v = malloc(argc * sizeof(char *));
11 for (i = 0; i < argc; i ++)
12 {
13 v[i] = malloc(strlen(argv[i]) + 1);
14 strcpy(v[i], argv[i]);
15 }
16
17 fp = fopen("text.txt", "w");
18 for (i = 0; i < argc; i ++)
19 {
20 int len = strlen(v[i]);
21 printf("%d %s\n", len, v[i]);
22 fwrite(&len, 1, sizeof(int), fp);
23 fwrite(v[i], 1, len, fp);
24 }
25 fclose(fp);
26
27 fp = fopen("text.txt", "r");
28 for (i = 0; i < argc; i ++)
29 {
30 int len;
31 char *buf;
32
33 fread(&len, 1, sizeof(int), fp);
34 buf = malloc(len + 1);
35 fread(buf, 1, len, fp);
36 buf[len] = 0;
37 printf("%d %s\n", len, buf);
38 free(buf);
39 free(v[i]);
40 }
41 free(v);
42 fclose(fp);
43
44 while (*environ)
45 printf("%s\n", *environ ++);
46
47 return 0;
48 }
所有库中函数的声明、类型的声明都放在了头文件minicrt.h中,没有像标准的库那样对每类库函数的声明放在单独的头文件中,如文件操作放在stdio.h中。测试程序中基本上都用到了我们前面编写过的函数,所以对于测试我们的库是最适合不过了。
要使用库,首先我们先要用前面编写的代码文件建立一个库,怎么建立呢?我们可以用linux下的ar命令来建立一个静态库,具体的可以见下面的命令。之所以用静态库,因为这样可以省略很多不必要的工作,我们的目的仅仅为了了解库的原理和关键技术。而动态库还有很多其它方面的知识,包括装载、运行时链接等,不过了解这些工作原理正是下面要做的工作了。
cc -c -g -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c test.c
ar -rs minicrt.a malloc.o stdio.o string.o
“-fno-builtin”指关闭GCC内置函数功能,默认情况下GCC会把strlen、strcmp等这些常用函数展开成它内部的实现。
"-nostdlib"不使用任何来自Glibc、GCC的库文件和启动文件,它包含了-nostartfiles这个参数。
"-fno-stack-protector"是指关闭堆栈保护功能,最近版本的GCC会在vfprintf这样的变长参数中插入堆栈保护函数,如果不关闭,使用自己写的库时会报“__stack_chk_fail”函数未定义错误。
其中entry.c是在编写简单的c运行库(一)中说的入口函数实现,malloc.c中是有关堆的初始化和申请释放堆的函数,stdio.c包含编写简单的c运行库(二)中有关文件操作的函数,string.c包含本文中说的字符串函数的实现,test.c中则是我们的测试代码。
链接测试程序时不能使用c的标准库,要用自己写的minicrt.a库,具体命令为:
ld -static -g -e MiniCrtEntry entry.o test.o minicrt.a -o test
"-e"参数是指定入口函数,我们使用自己实现的入口函数MiniCrtEntry。
运行的结果如下:
cc@localhostmimicrt]$./test
6 ./test
6 ./test
XDG_SESSION_ID=248
HOSTNAME=localhost.localdomain
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=192.168.1.161 62555 22
SSH_TTY=/dev/pts/0
USER=cc
LD_LIBRARY_PATH=/usr/local/lib
.
.
.
正如测试程序所希望的那样,程序打印出了命令行参数的总字节数,命令行参数,环境变量。可以说这个库基本上是正确的。
3 总结
编写简单的c运行库到这里基本就结束了,虽然只是实现了一个很小的库,不过麻雀虽小,五脏俱全,虽然没有真实c标准库那么的高效、完全,但至少这个库实现了c标准库的核心部分,有了这个小型库,对于扩展它的其它功能还是比较容易的。实现这个库还是比较的简单,因为有《程序员自我修养》这本书作为参考,不过这边书中所实现的linux中c++运行库的全局构造和析构机制,我在linux中按它说的实现,却发现结果和它说的不太一样,test.o中的.ctors节并没有合并到crtbegin.o和crtend.o的.ctors节之间,而是合并到crtbegin.o和crtend.o的.ctors节的下面去了,至于为什么会这样,我依然没有找到这个答案,希望有人按《程序员自我修养》实现过linux下的c++库的人帮忙解惑或者讨论下。
原文:http://www.cnblogs.com/chengxuyuancc/archive/2013/06/07/3123550.html