13.2 如何使用Mini CRT
通过上面的章节,我们已经基本实现了一个可以使用的Mini CRT,它虽然小但是却能支持大部分常用的CRT函数,使得程序可以脱离Glibc和MSVC CRT,仅依赖于Mini CRT就可以运行。而且Mini CRT还有一个惊人的特性那就是它是跨平台的,它可以运行在两个操作系统下面。有了上面章节中的实现原理及源代码之后,在这一节中将介绍如何使用Mini CRT。
一般一个CRT提供给最终用户时往往有两部分,一部分是CRT的库文件部分,用于与用户程序进行链接,如Glibc提供了两个版本的库文件:静态Glibc库libc.a和动态Glibc库libc.so;MSVC CRT也提供了静态和动态版本,libcmt.lib与msvcrt90.dll。CRT的另外一部分就是它的头文件,包含了使用该CRT所需要的所有常数定义、宏定义及函数声明,通常CRT都会有很多个头文件。
Mini CRT也将以库文件和头文件的形式提供给用户。首先我们建立一个minicrt.h的头文件,然后将所有相关的常数定义、宏定义,以及Mini CRT所实现的函数声明等放在该头文件里。当用户程序使用Mini CRT时,仅需要#include "minicrt.h"即可,而无须像标准的CRT一样,需要独立的包含相关文件,比如"stdio.h"、"stdlib.h"等。minicrt.h的内容如清单13-6所示。
清单13-6 minicrt.h
#ifndef __MINI_CRT_H__
#define __MINI_CRT_H__
#ifdef __cplusplus
extern "C" {
#endif
// malloc
#ifndef NULL
#define NULL (0)
#endif
void free(void* ptr);
void* malloc( unsigned size );
static int brk(void* end_data_segment);
int mini_crt_init_heap();
// 字符串
char* itoa(int n, char* str, int radix);
int strcmp (const char * src, const char * dst);
char *strcpy(char *dest, const char *src);
unsigned strlen(const char *str);
// 文件与IO
typedef int FILE;
#define EOF (-1)
#ifdef WIN32
#define stdin ((FILE*)(GetStdHandle(STD_INPUT_HANDLE)))
#define stdout ((FILE*)(GetStdHandle(STD_OUTPUT_HANDLE)))
#define stderr ((FILE*)(GetStdHandle(STD_ERROR_HANDLE)))
#else
#define stdin ((FILE*)0)
#define stdout ((FILE*)1)
#define stderr ((FILE*)2)
#endif
int mini_crt_init_io();
FILE* fopen( const char *filename,const char *mode );
int fread(void* buffer, int size, int count, FILE *stream);
int fwrite(const void* buffer, int size, int count, FILE *stream);
int fclose(FILE* fp);
int fseek(FILE* fp, int offset, int set);
// printf
int fputc(int c,FILE *stream );
int fputs( const char *str, FILE *stream);
int printf (const char *format, ...);
int fprintf (FILE *stream, const char *format, ...);
// internal
void do_global_ctors();
void mini_crt_call_exit_routine();
// atexit
typedef void (*atexit_func_t )( void );
int atexit(atexit_func_t func);
#ifdef __cplusplus
}
#endif
#endif // __MINI_CRT_H__
接下来的问题是如何编译得到库文件了。由于动态库的实现比静态库要复杂,所以Mini CRT仅仅以静态库的形式提供给最终用户,在Windows下它是minicrt.lib;在Linux下它是minicrt.a。在不同平台下编译和制作库文件的步骤如下所示,Linux下的命令行为:
$gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c
$ar -rs minicrt.a malloc.o printf.o stdio.o string.o
- 这里的-fno-builtin参数是指关闭GCC的内置函数功能,默认情况下GCC会把strlen、strcmp等这些常用函数展开成它内部的实现。
- -nostdlib表示不使用任何来自Glibc、GCC的库文件和启动文件,它包含了-nostartfiles这个参数。
- -fno-stack-protector是指关闭堆栈保护功能,最近版本的GCC会在vfprintf这样的变长参数函数中插入堆栈保护函数,如果不关闭,我们在使用Mini CRT时会发生"__stack_chk_fail"函数未定义的错误。
在Windows下,Mini CRT的编译方法如下:
>cl /c /DWIN32 /GS- entry.c malloc.c printf.c stdio.c string.c
>lib entry.obj malloc.obj printf.obj stdio.obj string.obj /OUT:minicrt.lib
- /DWIN32表示定义WIN32这个宏,这也正是在代码中用于区分平台的宏。
- /GS- 表示关闭堆栈保护功能,MSVC和GCC一样也会在不定参数中插入堆栈保护功能。不管这个功能会不会在最后链接时发生"__security_cookie"和"__security_check_ cookie"符号未定义错误。
为了测试Mini CRT是否能够正常运行,我们专门编写了一段测试代码,用于测试Mini CRT的功能,如清单13-7所示。
清单13-7 test.c
#include "minicrt.h"
int main(int argc, char* argv[])
{
int i;
FILE* fp;
char** v = malloc(argc*sizeof(char*));
for(i = 0; i < argc; ++i) {
v[i] = malloc(strlen(argv[i]) + 1);
strcpy(v[i], argv[i]);
}
fp = fopen("test.txt","w");
for(i = 0; i < argc; ++i) {
int len = strlen(v[i]);
fwrite(&len, 1, sizeof(int), fp);
fwrite(v[i],1, len, fp);
}
fclose(fp);
fp = fopen("test.txt","r");
for(i = 0; i < argc; ++i) {
int len;
char* buf;
fread(&len, 1, sizeof(int), fp);
buf = malloc(len + 1);
fread(buf, 1, len, fp);
buf[len] = '\0';
printf("%d %s\n", len, buf);
free(buf);
free(v[i]);
}
fclose(fp);
}
这段代码用到了Mini CRT中绝大部分函数,包括malloc、free、fopen、fclose、fread、fwrite、printf,并且测试了main参数。它的作用就是将main的参数字符串都保存到文件中,然后再读取出来,由printf显示出来。在Linux下,可以用下面的方法编译和运行test.c:
$gcc -c -ggdb -fno-builtin -nostdlib -fno-stack-protector test.c
$ld -static -e mini_crt_entry entry.o test.o minicrt.a –o test
$ ls -l test
-rwxr-xr-x 1 yujiazi yujiazi 5083 2008-08-19 21:59 test
$ ./test arg1 arg2 123
6 ./test
4 arg1
4 arg2
3 123
- -e mini_crt_entry用于指定入口函数。
可以看到静态链接Mini CRT最后输出的可执行文件只有5083个字节,这正体现出了Mini CRT的"迷你"之处,而如果静态链接Glibc时,最后可执行文件则约为538KB。在Windows下,编译和运行test.c的步骤如下:
>cl /c /DWIN32 test.c
>link test.obj minicrt.lib kernel32.lib /NODEFAULTLIB /entry:mini_crt_entry
>dir test.exe
…
2008-08-19 22:05 5,120 test.exe
..
>dumpbin /IMPORTS test.exe
Microsoft (R) COFF/PE Dumper Version 9.00.21022.08
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file test.exe
File Type: EXECUTABLE IMAGE
Section contains the following imports:
KERNEL32.dll
402000 Import Address Table
402050 Import Name Table
0 time date stamp
0 Index of first forwarder reference
16F GetCommandLineA
104 ExitProcess
454 VirtualAlloc
23B GetStdHandle
78 CreateFileA
368 ReadFile
48D WriteFile
43 CloseHandle
3DF SetFilePointer
Summary
1000 .data
1000 .rdata
1000 .text
>test.exe arg1 arg2 123
8 test.exe
4 arg1
4 arg2
3 123
与Linux类似,Windows下使用Mini CRT链接的可执行文件也非常小,只有5120字节。如果我们使用dumpbin查看它的导入函数可以发现,它仅依赖于Kernel32.DLL,也就是说它的确是绕过了MSVC CRT的运行库msvcr90.dll(或msvcr90d.dll)。