zoukankan      html  css  js  c++  java
  • 程序员的自我修养——第十一章——运行库

    程序从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函数负责填充缓冲。

  • 相关阅读:
    C++ 17
    C++ 11
    mysql统计某一个数据库中有几张表
    poj2636---Electrical Outlets(插线板)
    poj2608---几个字母映射到同一个数字
    poj2583---Series Determination
    poj2578---三个数中找出第一个大于168的
    poj2521---lose money
    poj2538---字符匹配(映射)
    poj2509---抽k根烟就换一支,求能抽烟的总数
  • 原文地址:https://www.cnblogs.com/zhuyp1015/p/2496701.html
Copyright © 2011-2022 走看看