程序员的自我修养——第十一章——运行库
程序从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++运行库在多线程下的问题:
- errno,errno是全局变量,多线程并发的时候,容易出问题
- 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()这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函数负责填充缓冲。