【程序员的自我修养】第11章 运行库
来源:互联网 发布:java file类能干嘛 编辑:程序博客网 时间:2024/05/16 07:31
第11章 运行库
入口函数和程序初始化
一个程序运行步骤大体如下:
l OS在创建进程之后,把控制权交给程序的入口函数
l 入口函数对运行库和程序运行环境进行初始化,包括堆、IO、线程、全局变量构造
l 入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分
l main函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭IO等,然后进行系统调用结束进程
Glibc的入口函数
_start,ld链接器默认的连接脚本指定,也可以自己设定
在调用_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初始化工作比较复杂,主要进行如下几个工作:
l 建立打开的文件表
l 如果能够继承自父进程,那么从父进程获取继承的句柄
l 初始化标准输入输出
C语言运行库(C Runtime Library):
C运行库大致包含如下功能:
l 启动与退出:包括入口函数及入口函数所依赖的其他函数等
l 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现
l I/O:I/O功能的封装和实现
l 堆:堆的封装和实现
l 语言实现:语言中的一些特殊功能的实现
l 调试:实现调试功能的代码
C语言的标准库:(ANSI C的标准库由24个C头文件组成)
l 标准输入和输出(stdio.h)
l 文件操作(stdio.h)
l 字符操作(ctype.h)
l 字符串操作(string.h)
l 数学函数(math.h)
l 资源管理(stdlib.h)
l 格式转换(stdlib.h)
l 时间/日期(time.h)
l 断言(assert.h)
l 各种类型上的常数(limits.h & float.h)
l 变长参数(stdarg.h)
l 非局部跳转(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语言运行库的一部分,但是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++运行库在多线程下的问题:
l errno,errno是全局变量,多线程并发的时候,容易出问题
l strtok()等函数都会使用函数内部的局部静态变量来存储字符串的位置,不同线程调用这个函数会将它内部的局部静态变量弄混乱
l malloc / new 与 free / delete: 堆分配/释放函数或关键字在不加锁的情况下是线程不安全的。
l printf/fprintf 及其他IO函数:流输出函数同样是线程不安全的,因为它们共享了同一个控制台或文件输出。
l 其他线程不安全函数:包括与信号相关的一些函数
为了解决C标准库在多线程环境下的窘迫处境,许多编译器附带了多线程版本的运行库。在MSVC中,可以用/MT或/MTd等参数指定多线程运行库。
针对多线程运行环境CRT的改进:
l 使用TLS
l 加锁,在多线程版本的运行库malloc/new前后不进行加锁也不会出现并发冲突
l 改进函数的调用方式(比如):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函数负责填充缓冲。
- 【程序员的自我修养】第11章 运行库
- 程序员的自我修养-运行库
- 程序员的自我修养: 运行库与多线程
- 程序员的自我修养---C/C++运行库
- 程序员的自我修养---C/C++运行库
- 程序员的自我修养——运行库
- 程序员的自我修养:MiniCRT自制C语言运行库
- 【程序员的自我修养】第1章 温故而知新
- 【程序员的自我修养】第2章 编译和链接
- 【程序员的自我修养】第4章 静态链接
- 【程序员的自我修养】第5章 Windows PE/COFF
- 【程序员的自我修养】第7章 动态链接
- 【程序员的自我修养】第10章 内存
- 【程序员的自我修养】第8章 Linux共享库的组织
- 程序员的自我修养
- 程序员的自我修养
- 程序员的自我修养
- 程序员的自我修养
- 笔记本禁用自带键盘攻略-------针对shift默认按下的解决方案
- 宏的常见应用领域
- 一句话点评国内外主流BI工具
- Android制作的一个通讯录
- [MINA2.0源码](三)服务端建立连接——NioSocketSession
- 【程序员的自我修养】第11章 运行库
- 日期工具类,计算周月季度相关值
- 如何让VISIO打开多个窗口
- 分析iMatrix平台的xml和jbpm原生的xml的区别
- linux自启动apache
- How to Install Android on Your PC
- Android调用Google Map
- C语言 二维数组(指针)动态分配和释放
- C#编写windows 服务 安装及卸载