【程序员的自我修养】第11章 运行库

来源:互联网 发布:java file类能干嘛 编辑:程序博客网 时间:2024/05/16 07:31

11章 运行库

入口函数和程序初始化

一个程序运行步骤大体如下:

OS在创建进程之后,把控制权交给程序的入口函数

入口函数对运行库和程序运行环境进行初始化,包括堆、IO、线程、全局变量构造

入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分

main函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭IO等,然后进行系统调用结束进程

Glibc的入口函数

_startld链接器默认的连接脚本指定,也可以自己设定

在调用_start前,装载器把用户参数和环境变量压入栈中,按照其压栈的方法,实际上栈顶的元素是argc,而接着其下就是argc和环境变量数组。

_start大概的功能可以用下面的代码描述:

void _start()

{

%ebp = 0;

int argc = pop from stack

char ** argv = top of stack;

__libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,

edx, top of stack);

}

其中argv除了指向参数表之外,还隐含紧接着环境变量表。这个环境变量表在__libc_start_main里从argv内部提取出来。

main函数和启动例程 

源文档 < http://learn.akae.cn/media/ch19s02.html > 

Linux C编程一站式学习

源文档 < http://learn.akae.cn/media/index.html > 

main函数和启动例程 是 Linux C编程一站式学习 的第 十九 章

MSVC的入口函数

int  mainCRTStartup(void) 

在该函数中使用了alloca进行内存分配,这是因为堆还没有初始化,而alloca是唯一可以不使用堆的动态分配机制的函数。

alloca可以再栈上分配任曦大小的空间(只要栈允许),并且在函数放回的时候自动释放,好像局部变量一样。

mainCRTStartup 的总体流程就是:

1.初始化和OS版本有关的全局变量

2.初始化堆

3.初始化I/O

4.获取命令行参数和环境变量

5.初始化C库的一些数据

6.调用main并记录返回值

7.检查错误并将main的返回值返回

MSVC CRT 的入口函数初始化

MSVC的入口函数初始化主要包含两部分,堆初始化和I/O初始化。MSVC的对初始化由函数_heap_init完成(调用HeapCreate)。

I/O初始化工作比较复杂,主要进行如下几个工作:

建立打开的文件表

如果能够继承自父进程,那么从父进程获取继承的句柄

初始化标准输入输出

C语言运行库(C Runtime Library):

C运行库大致包含如下功能:

启动与退出:包括入口函数及入口函数所依赖的其他函数等

标准函数:由C语言标准规定的C语言标准库所拥有的函数实现

I/OI/O功能的封装和实现

堆:堆的封装和实现

语言实现:语言中的一些特殊功能的实现

调试:实现调试功能的代码

C语言的标准库:(ANSI C的标准库由24C头文件组成)

标准输入和输出(stdio.h

文件操作(stdio.h

字符操作(ctype.h

字符串操作(string.h

数学函数(math.h

资源管理(stdlib.h

格式转换(stdlib.h

时间/日期(time.h

断言(assert.h

各种类型上的常数(limits.h & float.h

变长参数(stdarg.h

非局部跳转(setjmp.h

变长参数函数

#include <stdio.h>

#include <stdarg.h>

int sum(int num,...)

{

int *p = &num + 1;

int ret = 0;

while(num--)

ret += *p++;

return ret;

}

int main()

{

int num = 3;

printf("var argu sum = %d\n",sum(num,3,6,9));

return 0;

}

root@ubuntu:~/Desktop/ezCode# gcc -o var_arg var_arg.c

root@ubuntu:~/Desktop/ezCode# ./var_arg

var argu sum = 18

变长参数宏:

GCC编译器下,变长参数宏可以使用“##”宏字符串连接操作实现,比如:

#define  print(args...)  fprintf(stdout, ##args)

那么print("%d %s", 123, "hello")就会被展开成:

fprintf(stdout, "%d %s",123, "hello");

MSVC下,我们可以使用__VA_ARGS__这个编译器内置宏,比如:

#define  printf(...)  fprintf(stdout, __VA_ARGS__)

/*printf_vars.c*/

#include <stdio.h>

#define print(args...) fprintf(stdout,##args)

int main()

{

        int a = 10;

        char b[] = "test";

        print("%d %s\n",a,b);

        return 0;

}

root@ubuntu:~/Desktop/ezCode# gcc -o printf_vars printf_vars.c

root@ubuntu:~/Desktop/ezCode# ./printf_vars

10 test

/*jmp.c*/

#include <setjmp.h>

#include <stdio.h>

jmp_buf b;

void f()

{

        longjmp(b, 1);

}

int main()

{

        if (setjmp(b))

        {

                printf("World!\n");

        }

        else

        {

                printf("Hello ");

                f();

        }

        return 0;

}

root@ubuntu:~/Desktop/ezCode# ./setjump

Hello World!

setjmp正常返回时,返回0, 因此会打印出"Hello"字样.longjmp的作用,就是 让程序的执行流回到当初setjmp返回的时刻 ,并且返回由longjmp指定的返回值(longjmp的参数2), 也就是1,自然接着会打印出"World!"并退出

线程操作并不是标准C语言运行库的一部分,但是glibcMSVCRT都包含了线程操作的库函数.

glibc有一个可选的库pthread库中的pthread_create()函数可以用来创建线程;而MSVCRT中可以使用_beginthread()函数来创建线程。

Glibc启动文件:

Crt1.o里面包含的就是程序的入口函数_start, 由它负责调用__libc_start_main初始化libc并调用main函数进入震中的程序主体。

由于需要构造和析构全局变量,运行库在每个目标文件中引入了两个域初始化相关的段“.init”和“.finit”。因此引入了crti.ocrtn.o这两个目标文件。

root@ubuntu:/# objdump -dr /usr/lib/crti.o

/usr/lib/crti.o:     file format elf32-i386

Disassembly of section .init:

00000000 <_init>:

   0:        55                           push   %ebp

   1:        89 e5                        mov    %esp,%ebp

   3:        53                           push   %ebx

   4:        83 ec 04                     sub    $0x4,%esp

   7:        e8 00 00 00 00               call   c <_init+0xc>

   c:        5b                           pop    %ebx

   d:        81 c3 03 00 00 00            add    $0x3,%ebx

f: R_386_GOTPC        _GLOBAL_OFFSET_TABLE_

  13:        8b 93 00 00 00 00            mov    0x0(%ebx),%edx

15: R_386_GOT32        __gmon_start__

  19:        85 d2                        test   %edx,%edx

  1b:        74 05                        je     22 <_init+0x22>

  1d:        e8 fc ff ff ff               call   1e <_init+0x1e>

1e: R_386_PLT32        __gmon_start__

Disassembly of section .fini:

00000000 <_fini>:

   0:        55                           push   %ebp

   1:        89 e5                        mov    %esp,%ebp

   3:        53                           push   %ebx

   4:        83 ec 04                     sub    $0x4,%esp

   7:        e8 00 00 00 00               call   c <_fini+0xc>

   c:        5b                           pop    %ebx

   d:        81 c3 03 00 00 00            add    $0x3,%ebx

f: R_386_GOTPC        _GLOBAL_OFFSET_TABLE_

root@ubuntu:/# objdump -dr /usr/lib/crtn.o

/usr/lib/crtn.o:     file format elf32-i386

Disassembly of section .init:

00000000 <.init>:

   0:        58                           pop    %eax

   1:        5b                           pop    %ebx

   2:        c9                           leave 

   3:        c3                           ret   

Disassembly of section .fini:

00000000 <.fini>:

   0:        59                           pop    %ecx

   1:        5b                           pop    %ebx

   2:        c9                           leave 

   3:        c3                           ret   

连接器的输入文件顺序一般是:

ld   crt1.o   crti.o   [usrer_objects]  [system_libraries]  crtn.o

当希望使用自己的libccrt1.o等启动文件,以替代系统默认的文件。GCC提供了两个参数“-nostartfile”和“-nostdlib”分别用来 取消默认的启动文件和C语言运行库 

MSVC CRT

运行库与多线程

线程的私有空间:栈、线程局部存储(TLS)、寄存器

C/C++运行库在多线程下的问题:

errnoerrno是全局变量,多线程并发的时候,容易出问题

strtok()等函数都会使用函数内部的局部静态变量来存储字符串的位置,不同线程调用这个函数会将它内部的局部静态变量弄混乱

malloc / new  与 free / delete: 堆分配/释放函数或关键字在不加锁的情况下是线程不安全的。

printf/fprintf 及其他IO函数:流输出函数同样是线程不安全的,因为它们共享了同一个控制台或文件输出。

其他线程不安全函数:包括与信号相关的一些函数

为了解决C标准库在多线程环境下的窘迫处境,许多编译器附带了多线程版本的运行库。在MSVC中,可以用/MT/MTd等参数指定多线程运行库。

针对多线程运行环境CRT的改进:

使用TLS

加锁,在多线程版本的运行库malloc/new前后不进行加锁也不会出现并发冲突

改进函数的调用方式(比如)strtok() (MSVC)strtok_s(), (Glibc)strtok_r()

 

线程局部存储的实现:

对于TLS的用法很简单,如果要定义一个全局变量为TLS类型的,只需要在它的定义前加上相应的关键字即可。

对于GCC来说这个关键字是:__thread,我们可以这样顶一个一个TLS的全局变量:

__thread int number

对于MSVC来说,相应的关键字为__declspec(thread):

__declspec(thread) int number;

以上方法往往被称为隐式的TLS

Windows平台上,系统提供了TlsAlloc()TlsGetValue()、 TlsSetValue()TlsFree()4API函数用于显式TLS变量的申请、取值、赋值和释放。

Linux下相应的函数为pthread库中的:pthread_key_create()pthread_getspecific()pthread_setspecific()pthread_key_delete()

Windows API CreateThread()和另一种MSVC CRT的函数_beginthread()_beginthreadex()来创建线程,但是这两种类型不能混用,容易造成内存泄露。

fread实现:

fread的函数声明:

size_t  fread(

void       *buffer,

size_t     elementSize,

size_t     count,

FILE        *stream

)

功能是尝试从文件流stream里读取count个大小为elementSize个字节的数据,存储在buffer里,返回实际读取的字节数。

BOOL ReadFile(

HANDLE hFile,

LPVOID lpBuffer,

DWORD nNumberOfBytesToRead,

LPWORD lpNumberOfBytesRead,

LPOVERLAPPED lpOverlapped

);

最后一个参数没用几乎可以忽略它。

如果要实现一个简单的fread,可以直接调用ReadFile而不用做额外的处理。

缓冲:

在进行文件读写的时候并不是每次读写的结果立刻输出到相应位置,而是将这些读写的内容存储在一个缓冲区中,当内容达到一定大小之后一次性写入。

与缓冲区操作相关的函数:

int fflush(FILE *stream) flush指定文件的缓冲,若参数为NULL,则flush所有文件的缓冲

int setvbuf(FILE *stream, char *buf, int mode, size_t size)

无缓冲模式:_IONBF 该文件不使用任何缓冲

行缓冲模式:_IOLBF 仅对文本模式打开的文件有效,所谓行,即是指每收到一个换行符(\n\r\n),就将缓冲flush

全缓冲模式:_IOFBF 仅当缓冲满时才进行flush

void setbuf(FILE *stream, char *buf) 等价于 (void)setvbuf(stream, buf, _IOFBF, BUFSIZE)

MSVC中:

fread()  -> _fread_nolock()  -> fread_s()  -> _fread_nolock_s

typedef struct _iobuf

{

char*        _ptr;

int        _cnt;

char*        _base;

int        _flag;

int        _file;

int        _charbuf;

int        _bufsiz;

char*        _tmpfname;

} FILE;

_base 字段指向一个字符数组,即这个文件的缓冲,而_bufsiz记录着这个缓冲的大小。_ptr指向buffer中第一个未读的字节。而_cnt记录剩余未读字节的个数。

_fread_nolock_s(): _read()函数用于真正从文件读取数据。_filbuf函数负责填充缓冲。


原创粉丝点击