程序员的自我修养——第十一章——运行库

程序从main开始的吗?

在执行main之前全局变量已经初始化,main函数的两个参数也被正确传了进来,堆和栈的初始化也已经完成,一些系统I/O也被初始化。

完成上面这些工作的函数称为入口函数(Entry Point)。一个典型的运行步骤大致如下:

·操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个函数。

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

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

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

 

Glibc的入口函数:

_start

在调用_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/O:I/O功能的封装和实现

·堆:堆的封装和实现

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

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

 

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

·标准输入和输出(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,...)//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

 

补充:vprintf

#include <stdio.h>
#include <stdarg.h>

void WriteFormatted (char * format, ...)
{
  va_list args;
  va_start (args, format);
  vprintf (format, args);
  va_end (args);
}

int main ()
{
   WriteFormatted ("Call with %d variable argument.\n",1);
   WriteFormatted ("Call with %d variable %s.\n",2,"arguments");

return 0;
}

result:

Call with 1 variable argument.

Call with 2 variable arguments.

 

 

 

变长参数宏:

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语言运行库的一部分,但是glibc和MSVCRT都包含了线程操作的库函数.

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

 

Glibc启动文件:

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

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

 

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

 

MSVC CRT

 

运行库与多线程

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

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

  1. errno,errno是全局变量,多线程并发的时候,容易出问题
  1. strtok()等函数都会使用函数内部的局部静态变量来存储字符串的位置,不同线程调用这个函数会将它内部的局部静态变量弄混乱
  1. malloc / new  与 free / delete: 堆分配/释放函数或关键字在不加锁的情况下是线程不安全的。
  1. printf/fprintf 及其他IO函数:流输出函数同样是线程不安全的,因为它们共享了同一个控制台或文件输出。
  1. 其他线程不安全函数:包括与信号相关的一些函数

 

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

 

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

  1. 使用TLS
  1. 加锁,在多线程版本的运行库malloc/new前后不进行加锁也不会出现并发冲突
  1. 改进函数的调用方式(比如):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()这4个API函数用于显式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函数负责填充缓冲。

 

程序员的自我修养——第十一章——运行库

你可能感兴趣的:(程序员)