链接、装载与库 --- 运行库实现

C语言运行库

Mini CRT应该具备CRT的基本功能以及遵循几个基本设计原则:

  • 首先Mini CRT应该以ANIS C的标准库为目标,尽量做到与其接口相一致。
  • 具有自己的入口函数(mini_crt_entry)。
  • 基本的进程相关操作(exit)。
  • 支持堆操作(malloc、free)。
  • 支持基本的文件操作(fopen、fread、fwrite、fclose、fseek)。
  • 支持基本的字符串操作(strcpy、strlen、strcmp)。
  • 支持格式化字符串和输出操作(printf、sprintf)。
  • 支持atexit()函数。
  • Mini CRT能够同时支持Windows和Linux两个操作系统。
  • Mini CRT的实现应该尽量简单,以展示CRT的实现为目的,并不追求功能和性能,基本上是“点到为止”

使用宏WIN32为标准来决定是Windows还是Linux:

#ifdef WIN32
//Windows 部分实现代码
#else
//Linux 部分实现代码
#endif

Mini CRT中所有函数的声明都放在minicrt.h中。

开始

从入口函数开始入手是个不错的选择。

  • 入口函数负责三部分工作:准备好程序运行环境及初始化运行库,调用main函数执行程序主体,清理程序运行后的各种资源。
  • 运行库为所有程序提供的入口函数应该相同,在链接程序时须要指定该入口函数名。

首先,须要确定入口函数的函数原型,为了简单起见,声明如下:
void mini_crt_entry(void)
mini_crt_entry的返回值没有意义,因为它永远不会返回,在它返回之前就会调用进程退出函数结束进程。
基本框架如下:

void mini_crt_entry(void)
{
    // 初始化部分,负责准备好程序运行的环境,包括准备main函数的参数、初始化运行库,包括堆、IO
    int ret = main()
    // 结束部分,负责清理程序运行资源
    exit(ret);
}

围绕这个基本框架,逐步扩展补充入口函数。

main参数

main函数的原型为:
int main(int argc, char* argv[]);
当进程被初始化时,它的堆栈结构中就保存着环境变量和传递给main函数的参数,可以通过ESP寄存器获得这两个参数。但是一旦进入mini_crt_entry之后,ESP寄存器会随着函数的执行而被改变,由堆栈帧的知识,可以知道EBP的内容就是进入函数后ESP + 4(4是因为函数第一条指令是push ebp)。那么可以推断出EBP – 4所指向的内容应该就是argc,而EBP – 8则就是argv。整个堆栈的分布可以如图所示 main Windows提供API GetCommandLine返回整个命令行参数字符串,然后分割成若干个参数,以符合argc,argv的格式。

CRT初始化

CRT初始化的主要是堆和IO部分。定义堆的初始化函数为mini_crt_heap_init();IO初始化函数为mini_crt_io_init()。这两个函数的返回值都是整数类型的,返回非0即表示初始化成功,否则表示失败。

结束部分

Mini CRT结束部分完成两项任务:一是调用由atexit()注册的退出回调函数,由mini_crt_exit_routine()实现,主要用来实现全局对象的析构,这里暂时不支持C++;二是实现结束进程。这两项任务都由exit()函数完成,这个函数在Linux的实现是调用Linux的1号系统调用实现进程结束,ebx表示进程退出码;而Windows则提供ExitProcess结束进程。
最终Mini CRT的入口函数mini_crt_entry的代码如下所示:

//entry.c
#include "minicrt.h"

#ifdef WIN32
#include <Windows.h>
#endif

extern int main(int argc, char* argv[]);
void exit(int);

static void crt_fatal_error(const char* msg)
{
    // printf("fatal error: %s", msg);
    exit(1);
}

void mini_crt_entry(void)
{
    int ret;

#ifdef WIN32
    int flag = 0;
    int argc = 0;
    char* argv[16]; // 最多16个参数
    char* cl = GetCommandLineA();
    
    // 解析命令行
    argv[0] = cl;
    argc++;
    while(*cl) {
        if(*cl == '\"')
            if(flag == 0) flag = 1;
            else flag = 0;
        else if(*cl == ' ' && flag == 0) {
            if(*(cl+1)) {
                argv[argc] = cl + 1;
                argc++;
            }
            *cl = '\0';
        }
        cl++;
    }
#else
    int argc;
    char** argv;

    char* ebp_reg = 0;
    // ebp_reg = %ebp
    asm("movl %%ebp,%0 \n":"=r"(ebp_reg));

    argc = *(int*)(ebp_reg + 4);
    argv = (char**)(ebp_reg + 8);

#endif

    if (!mini_crt_heap_init())
        crt_fatal_error("heap initialize failed");

    if (!mini_crt_io_init())
        crt_fatal_error("IO initialize failed");
    
    ret = main(argc,argv);
    exit(ret);
}

void exit(int exitCode)
{
    //mini_crt_call_exit_routine();
#ifdef WIN32
    ExitProcess(exitCode);
#else
    asm( "movl %0,%%ebx \n\t"
         "movl $1,%%eax \n\t"
         "int $0x80     \n\t" 
         "hlt           \n\t"::"m"(exitCode));
#endif
}

Windows版分割算法有问题,比如两个参数之间隔多个空格就会发生问题。

堆的实现

下一步的目标就是实现堆的操作,即malloc()和free()。堆的实现归纳为下面几条:

  • 实现一个以空闲链表算法为基础的堆空间分配算法。
  • 为了简单起见,堆空间大小固定为32MB,初始化之后空间不再扩展或缩小。
  • 在Windows平台下不使用HeapAlloc等堆分配算法,采用VirtualAlloc向系统直接申请32MB空间,由自己的堆分配算法实现malloc。
  • 在Linux平台下,使用brk将数据段结束地址向后调整32MB,将这块空间作为堆空间。

由brk/sbrk分配的内存和VirtualAlloc分配的一样,它们仅仅是分配了虚拟空间,这些空间一开始是不会提交的(即不分配物理页面),当进程试图访问某一个地址的时候,操作系统会检测到访问异常,并且为被访问的地址所在的页分配物理页面。
在某些人的“黑话”里,践踏(trample)一块内存指的是去读写这块内存的每一个字节。brk所分配的虚地址就是需要在践踏之后才会被操作系统自动地分配实际页面。所以很多时候按页需求分配(Page Demand Allocation)又被称为按践踏分配(Alloc On Trample, AOT)。
当用户要申请一块内存时,堆分配算法将遍历整个链表,直到找到一块足够大的空闲块,如果这个空闲块大小刚好等于所申请的大小,那么直接将这个空闲块标记为占用块,然后将它的地址返回给用户;如果空闲块大小大于所申请的大小,那么这个空闲块将被分割成两块,其中一块大小为申请的大小,标记为占用,另外一块为空闲块。当用户释放某一块空间时,堆分配算法会判别被释放块前后两个块是否为空闲块,如果是,则将它们合并成一个大的空闲块。
Mini CRT的堆分配算法源代如下所示:

// malloc.c
#include "minicrt.h"

typedef struct _heap_header
{
    enum {
        HEAP_BLOCK_FREE = 0xABABABAB,   // magic number of free block
        HEAP_BLOCK_USED = 0xCDCDCDCD,   // magic number of used block
    } type;                             // block type FREE/USED

    unsigned size;                        // block size including header
    struct _heap_header* next;
    struct _heap_header* prev;
} heap_header;

#define ADDR_ADD(a,o) (((char*)(a)) + o)
#define HEADER_SIZE (sizeof(heap_header))

static heap_header* list_head = NULL;

void free(void* ptr)
{
    heap_header* header = (heap_header*)ADDR_ADD(ptr, -HEADER_SIZE);
    if(header->type != HEAP_BLOCK_USED) 
        return;

    header->type = HEAP_BLOCK_FREE;
    if(header->prev != NULL && header->prev->type == HEAP_BLOCK_FREE) {
        // merge
        header->prev->next = header->next;
        if(header->next != NULL)
            header->next->prev = header->prev;
        header->prev->size += header->size;
 		header = header->prev;
    }

    if(header->next != NULL && header->next->type == HEAP_BLOCK_FREE) {
        // merge
        header->size += header->next->size;
        header->next = header->next->next;
    }
}

void* malloc( unsigned size )
{
    heap_header *header;

    if( size == 0 ) 
        return NULL;

    header = list_head;
    while(header != 0) {
        if(header->type == HEAP_BLOCK_USED) {
            header = header->next;
            continue;
        }
            
        if(header->size > size + HEADER_SIZE &&
                      header->size <= size + HEADER_SIZE * 2) {
            header->type = HEAP_BLOCK_USED;
        }
        if(header->size > size + HEADER_SIZE * 2) {
            // split
            heap_header* next = (heap_header*)ADDR_ADD(header,size + 
                                     HEADER_SIZE);
            next->prev = header;
            next->next = header->next;
            next->type = HEAP_BLOCK_FREE;
            next->size = header->size - (size + HEADER_SIZE);
            header->next = next;
            header->size = size + HEADER_SIZE;
            header->type = HEAP_BLOCK_USED;
            return ADDR_ADD(header,HEADER_SIZE);
        }
        header = header->next;
    }

    return NULL;
}

#ifndef WIN32
// Linux brk system call
static int brk(void* end_data_segment) {
    int ret = 0;
    // brk system call number: 45
    // in /usr/include/asm-i386/unistd.h:
    // #define __NR_brk 45
    asm( "movl $45, %%eax     \n\t"
         "movl %1, %%ebx    \n\t"
             "int $0x80         \n\t"
             "movl %%eax, %0    \n\t"
             : "=r"(ret): "m"(end_data_segment) );
}
#endif

#ifdef WIN32
#include <Windows.h>
#endif

int mini_crt_heap_init()
{
    void* base = NULL;
    heap_header *header = NULL;
    // 32 MB heap size
    unsigned heap_size = 1024 * 1024 * 32;

#ifdef WIN32
    base = VirtualAlloc(0,heap_size,MEM_COMMIT | MEM_RESERVE,PAGE_READWRITE);
    if(base == NULL)
        return 0;
#else
    base = (void*)brk(0);
    void* end = ADDR_ADD(base, heap_size);
    end = (void*)brk(end);
    if(!end)
        return 0;
#endif

    header = (heap_header*)base;

    header->size = heap_size;
    header->type = HEAP_BLOCK_FREE;
    header->next = NULL;
    header->prev = NULL;

    list_head = header;
    return 1;
}

IO与文件操作

在传统的C语言和UNIX里面,IO和文件是同一个概念,所有的IO都是通过对文件的操作来实现的。因此,只要实现了文件的基本操作(fopen、fread、fwrite、fclose和fseek),即完成了Mini CRT的IO部分。与堆的实现一样,需要为Mini CRT的IO部分设计实现的基本原则:

  • 仅实现基本的文件操作,包括fopen、fread、fwrite、fclose及fseek。
  • 为了简单起见,不实现缓冲(Buffer)机制。
  • 不对Windows下的换行机制进行转换,即“\r\n”与“\n”之间不进行转换。
  • 支持三个标准的输入输出stdin、stdout和stderr。
  • 在Windows下,文件基本操作可以使用API:CreateFile、ReadFile、WriteFile、CloseHandle和SetFilePointer实现。
  • Linux不像Windows那样有API接口,必须使用内嵌汇编实现open、read、write、close和seek这几个系统调用。
  • fopen时仅区分“r”、“w”和“+”这几种模式及它们的组合,不对文本模式和二进制模式进行区分,不支持追加模式(“a”)。

Mini CRT的IO部分实现源代码如下所示:

// stdio.c
#include "minicrt.h"

int mini_crt_io_init()
{
    return 1;
}

#ifdef WIN32
#include <Windows.h>

FILE* fopen( const char *filename,const char *mode )
{
    HANDLE hFile = 0;
    int access = 0;
    int creation = 0;

    if(strcmp(mode, "w") == 0) {
        access |= GENERIC_WRITE;
        creation |= CREATE_ALWAYS;
    }

    if(strcmp(mode, "w+") == 0) {
 		access |=  GENERIC_WRITE | GENERIC_READ;
        creation |= CREATE_ALWAYS;
    }
        
    if(strcmp(mode, "r") == 0) {
        access |=  GENERIC_READ;
        creation += OPEN_EXISTING;
    }

    if(strcmp(mode, "r+") == 0) {
        access |=  GENERIC_WRITE | GENERIC_READ;
        creation |= TRUNCATE_EXISTING;
    }


    hFile = CreateFileA(filename, access, 0, 0, creation, 0, 0);
    if (hFile == INVALID_HANDLE_VALUE)
        return 0;

    return (FILE*)hFile;
}

int fread(void* buffer, int size, int count, FILE *stream)
{
    int read = 0;
    if (!ReadFile( (HANDLE)stream, buffer, size * count, &read, 0))
        return 0;
    return read;
}

int fwrite(const void* buffer, int size, int count, FILE *stream)
{
    int written = 0;
    if (!WriteFile( (HANDLE)stream, buffer, size * count, &written, 0))
        return 0;
    return written;
}
int fclose(FILE* fp)
{
    return CloseHandle((HANDLE)fp);
}

int fseek(FILE* fp, int offset, int set)
{
    return SetFilePointer((HANDLE)fp, offset, 0, set);
}

#else // #ifdef WIN32

static int open(const char *pathname, int flags, int mode)
{
    int fd = 0;
    asm("movl $5,%%eax    \n\t"
        "movl %1,%%ebx    \n\t"
        "movl %2,%%ecx    \n\t"
        "movl %3,%%edx    \n\t"
        "int $0x80        \n\t"
        "movl %%eax,%0    \n\t":
        "=m"(fd):"m"(pathname),"m"(flags),"m"(mode));
}

static int read( int fd, void* buffer, unsigned size)
{
    int ret = 0;
    asm("movl $3,%%eax    \n\t"
        "movl %1,%%ebx    \n\t"
        "movl %2,%%ecx    \n\t"
        "movl %3,%%edx    \n\t"
        "int $0x80        \n\t"
        "movl %%eax,%0    \n\t":
        "=m"(ret):"m"(fd),"m"(buffer),"m"(size));
    return ret;
}

static int write( int fd, const void* buffer, unsigned size)
{
    int ret = 0;
    asm("movl $4,%%eax    \n\t"
        "movl %1,%%ebx    \n\t"
        "movl %2,%%ecx    \n\t"
        "movl %3,%%edx    \n\t"
        "int $0x80        \n\t"
        "movl %%eax,%0    \n\t":
        "=m"(ret):"m"(fd),"m"(buffer),"m"(size));
    return ret;
}

static int close(int fd)
{
    int ret = 0;
    asm("movl $6,%%eax    \n\t"
        "movl %1,%%ebx    \n\t"
        "int $0x80        \n\t"
        "movl %%eax,%0    \n\t":
        "=m"(ret):"m"(fd));
    return ret;
}

static int seek(int fd, int offset, int mode)
{
    int ret = 0;
    asm("movl $19,%%eax   \n\t"
        "movl %1,%%ebx    \n\t"
        "movl %2,%%ecx    \n\t"
        "movl %3,%%edx    \n\t"
        "int $0x80        \n\t"
        "movl %%eax,%0    \n\t":
        "=m"(ret):"m"(fd),"m"(offset),"m"(mode));
    return ret;
}

FILE *fopen( const char *filename,const char *mode )
{
    int fd = -1;
    int flags = 0;
    int access = 00700; // 创建文件的权限

// 来自于/usr/include/bits/fcntl.h
// 注意:以0开始的数字是八进制的
#define O_RDONLY             00
#define O_WRONLY             01
#define O_RDWR               02
#define O_CREAT            0100
#define O_TRUNC           01000
#define O_APPEND          02000

    if(strcmp(mode, "w") == 0)
        flags |= O_WRONLY | O_CREAT | O_TRUNC;

    if(strcmp(mode, "w+") == 0)
        flags |=  O_RDWR | O_CREAT | O_TRUNC;
        
    if(strcmp(mode, "r") == 0)
        flags |=  O_RDONLY;

    if(strcmp(mode, "r+") == 0)
        flags |=  O_RDWR | O_CREAT;

    fd = open(filename, flags, access);
    return (FILE*)fd;
}

int fread(void* buffer, int size, int count, FILE* stream)
{    
    return read((int)stream, buffer, size * count);
}

int fwrite(const void* buffer, int size, int count, FILE* stream)
{
    return write((int)stream, buffer, size * count);
}

int fclose(FILE* fp)
{
    return close((int)fp);
}

int fseek(FILE* fp, int offset, int set)
{
    return seek((int)fp, offset, set);
}

#endif

由于省略了很多内容,Mini CRT相当于仅仅是对系统调用或Windows API的一个简单包装,FILE结构也被省略,FILE*这个类型在Windows下实际上是内核句柄,而在Linux下则是文件描述符,它并不是指向FILE结构的地址。 值得一提的是,在Windows下,标准输入输出并不是文件描述符0、1和2,而是要通过一个叫做GetStdHandle的API获得。 由于省略了诸多实现内容,所以CRT IO部分甚至可以不要做任何初始化,于是IO的初始化函数mini_crt_init_io也形同虚设,仅仅是一个空函数而已。

字符串相关操作

字符串相关的操作也是CRT的一部分,包括计算字符串长度、比较两个字符串、整数与字符串之间的转换等。由于这部分功能无须涉及任何与内核交互,是纯粹的用户态的计算,所以它们的实现相对比较简单。几个字符串相关的操作如下:

//string.c
char* itoa(int n, char* str, int radix)
{
    char digit[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    char* p = str;
    char* head = str;
    if (!p || radix < 2 || radix > 36)
        return p;
    if (radix != 10 && n < 0)
        return p;
    if (n == 0)
    {
        *p++ = '0';
        *p = 0;
        return p;
    }
    if (radix == 10 && n < 0)
    {
        *p++ = '-';
        n = -n;
    }
    while (n)
    {
        *p++ = digit[n % radix];
        n /= radix;
    }
    *p = 0;
    
    for (--p; head < p; ++head, --p)
    {
        char temp = *head;
        *head = *p;
        *p = temp;
    }    
    return str;
}

int strcmp (const char * src, const char * dst)
{
    int ret = 0 ;
    unsigned char* p1 = (unsigned char*)src;
    unsigned char* p2 = (unsigned char*)dst;
    while( ! (ret = *p1 - *p2) && *p2)
        ++p1, ++p2;

    if ( ret < 0 )
    ret = -1 ;
    else if ( ret > 0 )
        ret = 1 ;
    return( ret );
}

char *strcpy(char *dest, const char *src)
{
    char* ret = dest;
    while (*src)
        *dest++ = *src++;
    *dest = '\0';
    return ret;
}

unsigned strlen(const char *str)
{
    int cnt = 0;
    if (!str)
        return 0;
    for (; *str != '\0'; ++str)
        ++cnt;
    return cnt;
}

格式化字符串

接下来实现的是CRT中一个如雷贯耳的函数,那就是printf:

  • printf实现仅支持%d、%s,且不支持格式控制(比如%08d)。
  • 实现fprintf和vfprintf,实际上printf是fprintf的特殊形式,即目标文件为标准输出的fprintf。
  • 实现与文件字符串操作相关的几个函数,fputc和fputs。

printf相关的实现代码如下所示:

//printf.c
#include "minicrt.h"

int fputc(int c,FILE *stream )
{
    if (fwrite(&c, 1, 1, stream) != 1)
        return EOF;
    else
        return c;
}
int fputs( const char *str, FILE *stream)
{
    int len = strlen(str);
    if (fwrite(str, 1, len, stream) != len)
        return EOF;
    else
        return len;
}

#ifndef WIN32
#define va_list char*
#define va_start(ap,arg) (ap=(va_list)&arg+sizeof(arg))
#define va_arg(ap,t) (*(t*)((ap+=sizeof(t))-sizeof(t)))
#define va_end(ap) (ap=(va_list)0)
#else
#include <Windows.h>
#endif

int vfprintf(FILE *stream, const char *format, va_list arglist)
{
    int translating = 0;
    int ret = 0;
    const char* p = 0;
    for (p = format; *p != '\0'; ++p)
    {
        switch (*p)
        {
        case '%':
            if (!translating)
                translating = 1;
            else
            {
                if (fputc('%', stream) < 0)
                    return EOF;
                ++ret;
                translating = 0;
            }
            break;
        case 'd':
            if (translating)    //%d
            {
                char buf[16];
                translating = 0;
                itoa(va_arg(arglist, int), buf, 10);
                if (fputs(buf, stream) < 0)
                    return EOF;
                ret += strlen(buf);
            }
            else if (fputc('d', stream) < 0)
                return EOF;
            else
                ++ret;
            break;
        case 's':
            if (translating)    //%s
            {
                const char* str = va_arg(arglist, const char*);
                translating = 0;
                if (fputs(str, stream) < 0)
                    return EOF;
                ret += strlen(str);
            }
            else if (fputc('s', stream) < 0)
                    return EOF;
            else
                ++ret;
            break;
        default:
            if (translating)
                translating = 0;
            if (fputc(*p, stream) < 0)
                return EOF;
            else
                ++ret;
            break;
        }
    }
    return ret;
}

int printf (const char *format, ...)
{
    va_list(arglist);
    va_start(arglist, format);
    return vfprintf(stdout, format, arglist);
}

int fprintf (FILE *stream, const char *format, ...) 
{
    va_list(arglist);
    va_start(arglist, format);
    return vfprintf(stream, format, arglist);
}

可以看到vfprintf是这些函数中真正实现字符串格式化的函数,实现它的主要复杂性来源于对格式化字符串的分析。在这里使用了一种简单的算法:

  • 定义模式:翻译模式/普通模式。
  • 循环整个格式字符串。
    • 如果遇到%。
      • 普通模式:进入翻译模式;
      • 翻译模式:输出%,退出翻译模式。
    • 如果遇到%后面允许出现的特殊字符(如d和s)。
      • 翻译模式:从不定参数中取出一个参数输出,退出翻译模式;
      • 普通模式:直接输出该字符。
    • 如果遇到其他字符:无条件退出翻译模式并输出字符。

如何使用Mini CRT

Mini CRT也将以库文件和头文件的形式提供给用户。首先建立一个minicrt.h的头文件,然后将所有相关的常数定义、宏定义,以及Mini CRT所实现的函数声明等放在该头文件里。当用户程序使用Mini CRT时,仅需要#include “minicrt.h”即可。
minicrt.h的内容如下所示:

//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的功能:

//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

在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

使用dumpbin查看它的导入函数可以发现,它仅依赖于Kernel32.DLL,也就是说它的确是绕过了MSVC CRT的运行库msvcr90.dll(或msvcr90d.dll)。

C++运行库实现

通常C++的运行库都是独立于C语言运行库的,比如Linux下C语言运行库为libc.so/libc.a,而C++运行库为(libstdc++.so/libstdc++.a);Windows的C语言运行库为libcmt.lib/msvcr90.dll,而C++运行库为libcpmt.lib/msvcp90.dll。一般这些C++的运行库都是依赖于C运行库的,它们仅包含对C++的一些特性的支持,比如new/delete、STL、异常处理、流(stream)等。但是它们并不包含诸如入口函数、堆管理、基本文件操作等这些特性,而这些也是C++运行库所必需的,比如C++的流和文件操作依赖于C运行库的基本文件操作,所以它必须依赖于C运行库。
本节将在Mini CRT的基础上实现一个支持C++的运行库,当然出于简单起见,将这个C++运行库的实现与Mini CRT合并到一起,而不是单独成为一个库文件。对C++的标准库进行简化,最终目标是实现一个能够成功运行如下C++程序代码的运行库:

// test.cpp
#include "iostream"
#include "string"

using namespace std;

int main(int argc, char* argv[])
{
    string* msg = new string("Hello World");
    cout << *msg << endl;
    delete msg;
    return 0;
}

上面这段程序用到了C++运行库的诸多功能,将所用到的特性列举如下:

  • string类的实现。
  • stream类的实现,包括操纵符(Manupilator)(endl)。
  • 全局对象构造和析构(cout)。
  • new/delete。

在实现Mini CRT对C++的支持时,遵循如下原则:

  • HelloWorld程序无须用到的功能就不实现,比如异常。
  • 尽量简化设计,尽量符合C++标准库的规范。
  • 对于可以直接在头文件实现的模块尽量在头文件中实现,以免诸多的类、函数的声明和定义造成代码量膨胀,不便于演示。
  • 支持Windows和Linux。虽然C++运行库几乎没有与系统相关的部分(全局构造和析构除外),C运行库已经将大部分系统相关部分封装成C标准库接口,C++运行库只须要调用这些接口即可。
  • 另外值得一提的是,模板是不需要运行库支持的,它的实现依赖于编译器和链接器,对运行库基本上没有要求。

new与delete

首先从比较简单的模块入手,全局new/delete操作的实现应该是最简单的部分。
那么new和delete究竟在C++中是一个什么样的地位呢?它们是编译器内置的操作吗?它们跟运行库有什么关系呢?为了解释这些问题,首先来看一小段代码:

class C {
};

int main()
{
    C* c = new C();
    return 0;
}

假如用GCC编译这段代码并且反汇编,将会看到new操作的实现:

$g++ -c hello.c
$objdump -dr hello.o

hello.o:     file format elf32-i386

Disassembly of section .text:

00000000 <main>:
   0:   8d 4c 24 04             lea    0x4(%esp),%ecx
   4:   83 e4 f0                and    $0xfffffff0,%esp
   7:   ff 71 fc                pushl  -0x4(%ecx)
   a:   55                      push   %ebp
   b:   89 e5                   mov    %esp,%ebp
   d:   51                      push   %ecx
   e:   83 ec 14                sub    $0x14,%esp
  11:   c7 04 24 01 00 00 00    movl   $0x1,(%esp)
  18:   e8 fc ff ff ff          call   19 <main+0x19>
                        19: R_386_PC32  _Znwj
  1d:   89 45 f8                mov    %eax,-0x8(%ebp)
  20:   b8 00 00 00 00          mov    $0x0,%eax
  25:   83 c4 14                add    $0x14,%esp
  28:   59                      pop    %ecx
  29:   5d                      pop    %ebp
  2a:   8d 61 fc                lea    -0x4(%ecx),%esp
  2d:   c3                      ret

可以看到,new操作的实现实际上是调用了一个叫做_Znwj的函数,如果用c++filt将这个符号反修饰(Demangle),可以看到它的真面目:

$c++filt _Znwj
operator new(unsigned int)

可知_Znwj是C++中的操作符函数operator new,一般被定义为:
void* operator new(unsigned int size);
所以实现new/delete,就是实现这两个操作符函数。而这两个函数的主要功能是申请和释放堆空间,这再容易不过了,因为在Mini CRT中已经实现了堆空间的申请和释放函数:malloc和free。代码如下所示:

//new_delete.cpp
extern "C" void* malloc(unsigned int);
extern "C" void free(void*);

void* operator new(unsigned int size)
{
    return malloc(size);
}

void operator delete(void* p)
{
    free(p);
}

void* operator new[](unsigned int size)
{
    return malloc(size);
}

void operator delete[](void* p)
{
    free(p);
}

上面的实现除了申请和释放堆空间之外,没有任何对象构造和析构的调用,其实对象的构造和析构是在new/delete之前/之后由编译器负责产生相应的代码进行调用的,new/delete仅仅负责堆空间的申请和释放,不负责构造和析构。
在真实的C++运行库中,new/delete的实现要比上面的复杂一些,它们除了使用malloc/free申请释放空间之外,还支持new_handler在申请失败时给予程序进行补救的机会、还可能会抛出bad_alloc异常等,由于Mini CRT并不支持异常,所以就省略了这些内容。

另外值得一提的是,在使用真实的C++运行库时,也可以使用上面这段代码自己实现new/delete,这样就会将原先C++运行库的new/delete覆盖,使得有机会在new/delete时记录对象的空间分配和释放,可以实现一些特殊的功能,比如检查程序是否有内存泄露。这种做法往往被称为全局new/delete操作符重载(Global new/delete operator overloading)。除了重载全局new/delete操作符之外,也可以重载某个类的new/delete,这样可以实现一些特殊的需求,比如指定对象申请地址(Replacement new),或者使用自己实现的堆算法对某个对象的申请/释放进行优化,从而提高程序的性能等,这方面的讨论在C++领域已经非常深入了,在此不一一展开了。

C++全局构造与析构

C++全局构造与析构的实现依赖于编译器、链接器和运行库三者共同的支持和协作。Mini CRT对于全局对象构造与析构的实现基于“运行库”中描述的Glibc和MSVC CRT,本质上没有多大的区别,仅仅是将它们简化到最简程度,保留本质而去除了一些繁琐的细节。
通过“运行库”可以得知,C++全局构造和析构的实现在Glibc和MSVC CRT中的原理十分相似,构造函数主要实现的是依靠特殊的段合并后形成构造函数数组,而析构则依赖于atexit()函数。这一节中将主要关注全局构造的实现,而把atexit()的实现留到下一节中。
全局构造对于MSVC来说,主要实现两个段“.CRT$XCA”和“.CRT$XCZ”,然后定义两个函数指针分别指向它们;而对于GCC来说,须要定义“.ctor”段的起始部分和结束部分,然后定义两个函数指针分别指向它们。真正的构造部分则只要由一个循环将这两个函数指针指向的所有函数都调用一遍即可。
MSVC CRT与Glibc在实现上稍有不同的是,MSVC CRT只需要一个目标文件就可以实现全局构造,编译器会按照段名将所有的输入段排序;而Glibc需要两个文件:ctrbegin.o和crtend.o,这两个文件在编译时必须位于输入文件的开始和结尾部分,所有在这两个文件之外的输入文件中的“.ctor”段就不会被正确地合并。
全局构造和析构的实现代码如下所示:

// ctors.cpp
typedef void (*init_func)(void);
#ifdef WIN32
#pragma section(".CRT$XCA",long,read)
#pragma section(".CRT$XCZ",long,read)

__declspec(allocate(".CRT$XCA")) init_func ctors_begin[] = { 0 };
__declspec(allocate(".CRT$XCZ")) init_func ctors_end[] = { 0 };

extern "C" void do_global_ctors()
{
  init_func* p = ctors_begin;
  while ( p < ctors_end )
    {
        if (*p != 0)
            (**p)();
        ++p;
    }
}
#else

void run_hooks();
extern "C" void do_global_ctors()
{
    run_hooks();
}
#endif

在.ctors.cpp中包含了Windows的全局构造的所有实现代码,但Linux的全局构造还需要crtbegin和crtend两个部分。这两个文件内容:

///crtbegin.cpp
#ifndef WIN32
typedef void (*ctor_func)(void);

ctor_func ctors_begin[1] __attribute__ ((section(".ctors"))) =
{
    (ctor_func) -1
};

void run_hooks()
{
    const ctor_func* list = ctors_begin;
    while ((int)*++list != -1)
        (**list)();
}
#endif 
//crtend.cpp
#ifndef WIN32
typedef void (*ctor_func)(void);
ctor_func crt_end[1] __attribute__ ((section(".ctors"))) = 
{
    (ctor_func) -1
};
#endif

atexit实现

atexit实现的基本思路很简单,使用一个链表把所有注册的函数存储起来,到exit()时将链表遍历一遍,执行其中所有的回调函数,Windows版的atexit的确可以按照这个思路实现。
Linux版的atexit要复杂一些,因为GCC实现全局对象的析构不是调用的atexit,而是调用的__cxa_atexit。为了兼容GCC,Mini CRT不得不实现它。它的定义与atexit()有所不同的是,__cxa_atexit所接受的参数类型和atexit不同:

typedef void (*cxa_func_t )( void* );
typedef void (*atexit_func_t )( void );
int __cxa_atexit(cxa_func_t func, void* arg, void*);
int atexit(atexit_func_t func);

__cxa_atexit所接受的函数指针必须有一个void*型指针作为参数,并且调用__cxa_atexit的时候,这个参数(void* arg)也要随着记录下来,等到要执行的时候再传递进去。最后一个参数可以忽略。
于是在设计链表时要考虑到这一点,链表的节点必须能够区分是否是atexit()函数__cxa_atexit()注册的函数,如果是__cxa_atexit()注册的函数,还要把回调函数的参数保存下来。定义链表节点的结构如下:

typedef struct _func_node
{
    atexit_func_t func;
    void* arg;
    int is_cxa;//若不为0,则表示这个节点是由`__cxa_atexit()`注册的回调函数,arg成员表示相应的参数
    struct _func_node* next;
} func_node;

atexit的实现:

// atexit.c
#include "minicrt.h"

typedef struct _func_node
{
    atexit_func_t func;
    void* arg;
    int is_cxa;
    struct _func_node* next;
} func_node;

static func_node* atexit_list = 0;

int register_atexit(atexit_func_t func, void* arg, int is_cxa)
{
    func_node* node;
    if (!func) return -1;

    node = (func_node*)malloc(sizeof(func_node));
    
    if(node == 0) return -1;

    node->func = func;
    node->arg = arg;
    node->is_cxa = is_cxa;
    node->next = atexit_list;
    atexit_list = node;
    return 0;
}

#ifndef WIN32
typedef void (*cxa_func_t )( void* );
int __cxa_atexit(cxa_func_t func, void* arg, void* unused)
{
    return register_atexit((atexit_func_t)func, arg, 1);
}
#endif

int atexit(atexit_func_t func)
{
    return register_atexit(func, 0, 0);
}

void mini_crt_call_exit_routine()
{
    func_node* p = atexit_list;
    for(; p != 0; p = p->next)
    {
        #ifdef WIN32
            p->func();
        #else
        if (p->is_cxa)
            ((cxa_func_t)p->func)(p->arg);
        else
            p->func();
        #endif
        free(p);
    }
    atexit_list = 0;
}

入口函数修改

由于增加了全局构造和析构的支持,那么需要对Mini CRT的入口函数和exit()函数进行修改,把对do_global_ctors()mini_crt_call_exit_routine()的调用加入到entry()和exit()函数中去。修改后的entry.c如下(省略一部分未修改的内容):

//entry.c

void mini_crt_entry(void)
{

    if (!mini_crt_heap_init())
        crt_fatal_error("heap initialize failed");

    if (!mini_crt_io_init())
        crt_fatal_error("IO initialize failed");
    
    do_global_ctors();

    ret = main(argc,argv);
    exit(ret);
}

void exit(int exitCode)
{
    mini_crt_call_exit_routine();
#ifdef WIN32
    ExitProcess(exitCode);
#else
    asm( "movl %0,%%ebx \n\t"
         "movl $1,%%eax \n\t"
         "int  $0x80    \n\t" 
         "hlt           \n\t"::"m"(exitCode));
#endif
}

stream与string

流和字符串是C++ STL的最基本的两个部分,这一节为Mini CRT增加string和stream的实现,在有了流和字符串之后,Mini CRT将最终宣告完成,可以考虑将它重命名为Mini CRT++。
string和stream的实现将遵循下列原则:

  • 不支持模板定制,即这两个类仅支持char字符串类型,不支持自定义分配器等,没有basic_string模板类。
  • 流对象仅实现ofstream,且没有继承体系,即没有ios_base、stream、ostream、fstream等类似的相关类。
  • 流对象没有内置的缓冲功能,即没有stream_buffer类支持。
  • cout作为ofstream的一个实例,它的输出文件是标准输出。
// string

namespace std {

    class string
    {
        unsigned len;
        char* pbuf;
        
    public:
        explicit string(const char* str);
        string(const string&);
        ~string();
        string& operator=(const string&);
        string& operator=(const char* s);
        const char& operator[](unsigned idx) const;
        char& operator[](unsigned idx);
        const char* c_str() const;
        unsigned length() const;
        unsigned size() const;
    };

    string::string(const char* str) :
        len (0), pbuf(0)
    {
        *this = str;
    }

    string::string(const string& s) : 
        len(0), pbuf(0)
    {
        *this = s;
    }
    string::~string()
    {
        if(pbuf != 0) {
            delete[] pbuf;
            pbuf = 0;
        }
    }

    string& string::operator=(const string& s)
    {
        if (&s == this)
            return *this;
        this->~string();
        len = s.len;
        pbuf = strcpy(new char[len + 1], s.pbuf);
        return *this;
    }

    string& string::operator=(const char* s)
    {
        this->~string();
        len = strlen(s);
        pbuf = strcpy(new char[len + 1], s);
        return *this;
    }

    const char& string::operator[](unsigned idx) const
    {
        return pbuf[idx];
    }
    char& string::operator[](unsigned idx)
    {
        return pbuf[idx];
    }
    const char* string::c_str() const
    {
        return pbuf;
    }
    unsigned string::length() const
    {
        return len;
    }
    unsigned string::size() const
    {
        return len;
    }
    ofstream& operator<<(ofstream& o, const string& s)
    {
        return o << s.c_str();
    }
}
// iostream
#include "minicrt.h"

namespace std {

class ofstream
{
    protected:
        FILE* fp;
        ofstream(const ofstream&);
    public:
        enum openmode{in = 1, out = 2, binary = 4, trunc = 8};

        ofstream();
        explicit ofstream(const char *filename, ofstream::openmode md = ofstream::out);
        ~ofstream();
        ofstream& operator<<(char c);
        ofstream& operator<<(int n);
        ofstream& operator<<(const char* str);
        ofstream& operator<<(ofstream& (*)(ofstream&));
        void open(const char *filename, ofstream::openmode md = ofstream::out); 
        void close();
        ofstream& write(const char *buf, unsigned size);
};

inline ofstream& endl(ofstream& o)
{
    return o << '\n';
}

class stdout_stream : public ofstream {
public:
    stdout_stream();
};

extern stdout_stream cout;
}
// iostream.cpp
#include "minicrt.h"
#include "iostream"

#ifdef WIN32
#include <Windows.h>
#endif

namespace std {

stdout_stream::stdout_stream() : ofstream() 
{
        fp = stdout;
}

stdout_stream cout;

ofstream::ofstream() : fp(0)
{
}

ofstream::ofstream(const char *filename, ofstream::openmode md) : fp(0)
{
    open(filename, md);
    
}
ofstream::~ofstream()
{
    close();
}
ofstream& ofstream::operator<<(char c)
{
    fputc(c, fp);
    return *this;
}
ofstream& ofstream::operator<<(int n)
{
    fprintf(fp, "%d", n);
    return *this;
}
ofstream& ofstream::operator<<(const char* str)
{
    fprintf(fp, "%s", str);
    return *this;
}

ofstream& ofstream::operator<<(ofstream& (*manip)(ofstream&))
{
    return manip(*this);
}

void ofstream::open(const char *filename, ofstream::openmode md)
{
    char mode[4];
    close();
    switch (md)
    {
    case out | trunc:
        strcpy(mode, "w");
        break;
    case out | in | trunc:
        strcpy(mode, "w+");
    case out | trunc | binary:
        strcpy(mode, "wb");
        break;
    case out | in | trunc | binary:
        strcpy(mode, "wb+");
    }
    fp = fopen(filename, mode);
}
void ofstream::close()
{
    if (fp)
    {
        fclose(fp);
        fp = 0;
    }
}

ofstream& ofstream::write(const char *buf, unsigned size)
{
    fwrite(buf, 1, size, fp);
    return *this;
}

}

如何使用Mini CRT++

首先展示在Windows下编译的方法:

$cl /c /DWIN32 /GS- entry.c malloc.c printf.c stdio.c string.c atexit.c
$cl /c /DWIN32 /GS- /GR- crtbegin.cpp crtend.cpp ctor.cpp new_delete.cpp iostream.cpp
$lib entry.obj malloc.obj printf.obj stdio.obj string.obj ctor.obj new_delete.obj atexit.obj iostream.obj /OUT:minicrt.lib

这里新增的一个编译参数为/GR-,它的意思是关闭RTTI功能,否则编译器会为有虚函数的类产生RTTI相关代码,在最终链接时会看到“const type_info::`vftable”符号未定义的错误。

为了能够在linux下正常运行,还需要建立一个新的源代码文件叫sysdep.cpp,用于定义linux平台相关的一个函数:

extern "C"{
	void * __dso_handle = 0;
}

这个函数用于处理共享库的全局对象的构造与析构。由于共享库可能在进程退出之前被卸载,为保证共享库的全局对象被正确的析构,GCC的__cxa_atexit()第三个参数用于标示这个析构函数属于哪个共享对象,它就是__dso_handle这个符号。minicrt不考虑对共享库的支持,为防止链接时出现符号未定义错误,定义这个符号为0。

Mini CRT++在Linux平台下编译的方法如下:

$gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c atexit.c
$g++ -c -nostdinc++ -fno-rtti -fno-exceptions -fno-builtin -nostdlib -fno-stack-protector crtbegin.cpp crtend.cpp ctor.cpp new_delete.cpp sysdep.cpp iostream.cpp sysdep.cpp
$ar -rs minicrt.a malloc.o printf.o stdio.o string.o ctor.o atexit.o iostream.o new_delete.o sysdep.o
  • -fno-rtti的作用与cl的/GR-作用一样,用于关闭RTTI。
  • -fno-exceptions的作用用于关闭异常支持,否则GCC会产生异常支持代码,可能导致链接错误。

在Windows下使用Mini CRT++的方法如下:

$cl /c /DWIN32 /GR- test.cpp
$link test.obj minicrt.lib kernel32.lib /NODEFAULTLIB /entry:mini_crt_entry

在Linux下使用Mini CRT++的方法如下:

$g++ -c -nostdinc++ -fno-rtti -fno-exceptions -fno-builtin -nostdlib -fno-stack-protector test.cpp
$ld -static -e mini_crt_entry entry.o crtbegin.o test.o minicrt.a crtend.o -o test

crtbegin.o和crtend.o在ld链接时位于用户目标文件的最开始和最后端,以保证链接的正确性。