Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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)。