PILE读书笔记_基础知识
Posted abc_begin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PILE读书笔记_基础知识相关的知识,希望对你有一定的参考价值。
程序的构成
Linux下二进制可执行程序的格式一般为ELF格式。 我们可以用readelf命令来读取二进制的信息。
ELF文件的主要内容就是由各个section及symbol表组成的。 下面来分别介绍这些字段的含义:
- .text:已编译程序的机器代码,为代码段, 用于保存可执行指令 。
- .rodata:只读数据,比如printf语句中的格式串和开关(switch)语句的跳转表。
- .data:已初始化的全局变量或静态变量。局部C变量在运行时被保存在栈中,既不出现在.data中,也不出现在.bss节中。
- .bss:未初始化或者初始化值为0的全局变量或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率在:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
- .symtab:一个符号表(symbol table),它存放在程序中被定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的表目。
- .rel.text:当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示链接器包含这些信息。
- .rel.data:被模块定义或引用的任何全局变量的信息。一般而言,任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要被修改。
- .debug:一个调试符号表,其有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的C源文件。只有以-g选项调用编译驱动程序时,才会得到这张表。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译驱动程序时,才会得到这张表。
- .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
- dynamic段: 用于保存动态链接信息。
- fini段: 用于保存进程退出时的执行程序。 当进程结束时, 系统会自动执行这部分代码 。
- init段: 用于保存进程启动时的执行程序。 当进程启动时, 系统会自动执行这部分代码。
- rodata段: 用于保存只读数据, 如const修饰的全局变量、 字符串常量。
- symtab段: 用于保存符号表。
二进制执行流程
下面是一个简单的代码,我们把它编译一下用strace命令来分析他的执行过程。
1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 int main( ) 5 { 6 int fd ; 7 int i = 0 ; 8 fd = open( “/tmp/foo”, O_RDONLY ) ; 9 if ( fd < 0 ) 10 i=5; 11 else 12 i=2; 13 return i; 14 }
过程如下:
1 execve("./main", ["./main"], [/* 43 vars */]) = 0
2 brk(0) = 0x9ac4000
3 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
4 mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7739000
5 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
6 open("/etc/ld.so.cache", O_RDONLY) = 3
7 fstat64(3, {st_mode=S_IFREG|0644, st_size=80682, ...}) = 0
8 mmap2(NULL, 80682, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7725000
9 close(3) = 0
10 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
11 open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
12 read(3, "\\177ELF\\1\\1\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\3\\0\\3\\0\\1\\0\\0\\0\\220o\\1\\0004\\0\\0\\0"..., 512) = 512
13 fstat64(3, {st_mode=S_IFREG|0755, st_size=1434180, ...}) = 0
14 mmap2(NULL, 1444360, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x56d000
15 mprotect(0x6c7000, 4096, PROT_NONE) = 0
16 mmap2(0x6c8000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x15a) = 0x6c8000
17 mmap2(0x6cb000, 10760, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x6cb000
18 close(3) = 0
19 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7724000
20 set_thread_area({entry_number:-1 -> 6, base_addr:0xb77248d0, limit:1048575, seg_32bit:1, contents:0, read_exec_ only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
21 mprotect(0x6c8000, 8192, PROT_READ) = 0
22 mprotect(0x8049000, 4096, PROT_READ) = 0
23 mprotect(0x4b0000, 4096, PROT_READ) = 0
24 munmap(0xb7725000, 80682) = 0
25 open("/tmp/foo", O_RDONLY) = -1 ENOENT (No such file or directory)
26 exit_group(5) = ?
strace跟踪程序与系统交互时产生的系统调用,以上每一行就对应一个系统调用,格式为:
系统调用的名称( 参数... ) = 返回值 错误标志和描述
另外,使用strace跟踪挂死程序,如果最后一行系统调用显示完整,程序在逻辑代码处挂死;如果最后一行系统调用显示不完整,程序在该系统调用处挂死。当程序挂死在系统调用处,我们可以查看相应系统调用的man手册,了解在什么情况下该系统调用会出现挂死情况。
上面strace命令打印出来的信息分析如下:
Line 1: 对于命令行下执行的程序,execve(或exec系列调用中的某一个)均为strace输出系统调用中的第一个。strace首先调用fork或clone函数新建一个子进程,然后在子进程中调用exec载入需要执行的程序(这里为./main)
Line 2: 以0作为参数调用brk,返回值为内存管理的起始地址(若在子进程中调用malloc,则从0x9ac4000地址开始分配空间)
Line 3: 调用access函数检验/etc/ld.so.nohwcap是否存在,如果ld.so.nohwcap存在, 则ld会加载其中未优化版本的库
Line 4: 使用mmap2函数进行匿名内存映射,以此来获取8192bytes内存空间,该空间起始地址为0xb7739000
Line 5: 调用access函数检验/etc/ld.so.preload是否存在,如果ld.so.preload存在,则ld会加载其中的库——在一些项目中, 我们需要拦截或替换系统调用或C库, 此时就会利用这个机制, 使用LD_PRELOAD来实现
Line 6: 调用open函数尝试打开/etc/ld.so.cache文件,返回文件描述符为3
Line 7: fstat64函数获取/etc/ld.so.cache文件信息
Line 8: 调用mmap2函数将/etc/ld.so.cache文件映射至内存
Line 9: close关闭文件描述符为3指向的/etc/ld.so.cache文件
Line12: 调用read,从/lib/i386-linux-gnu/libc.so.6该libc库文件中读取512bytes,这个就是C库
Line15: 使用mprotect函数对0x6c7000起始的4096bytes空间进行保护(PROT_NONE表示不能访问,PROT_READ表示可以读取)
Line24: 调用munmap函数,将/etc/ld.so.cache文件从内存中去映射,与Line 8的mmap2对应
Line25: 对应源码中使用到的唯一的系统调用——open函数,使用其打开/tmp/foo文件
Line26: 子进程结束,退出码为5
可重入函数
可重入就是可重复进入。不仅仅意味着可以重复进入, 还要求在进入后能成功执行。 这里的重复进入, 是指当前进程已经处于该函数中, 这时程序会允许当前进程的某个执行流程再次进入该函数, 而不会引发问题。 这里的执行流程不仅仅包括多线程, 还包括信号处理、 longjump等执行流程。 所以, 可重入函数一定是线程安全的, 而线程安全函数则不一定是可重入函数。
比如下面的代码就会造成死锁:
1 #include <stdlib.h> 2 #include <stdio.h> 3 #include <pthread.h> 4 #include <unistd.h> 5 #include <signal.h> 6 #include <sys/types.h> 7 8 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 9 static const char * const caller[2] = {"mutex_thread", "signal handler"}; 10 static pthread_t mutex_tid; 11 static pthread_t sleep_tid; 12 static volatile int signal_handler_exit = 0; 13 14 static void hold_mutex(int c) 15 { 16 printf("enter hold_mutex [caller %s]\\n", caller[c]); 17 pthread_mutex_lock(&mutex); 18 /* 这里的循环是为了保证锁不会在信号处理函数退出前被释放掉 19 */ 20 while (!signal_handler_exit && c != 1) { 21 sleep(5); 22 } 23 pthread_mutex_unlock(&mutex); 24 printf("leave hold_mutex [caller %s]\\n", caller[c]); 25 } 26 27 static void *mutex_thread(void *arg) 28 { 29 hold_mutex(0); 30 } 31 32 static void *sleep_thread(void *arg) 33 { 34 sleep(10); 35 } 36 37 static void signal_handler(int signum) 38 { 39 hold_mutex(1); 40 signal_handler_exit = 1; 41 } 42 43 int main() 44 { 45 signal(SIGUSR1, signal_handler); 46 pthread_create(&mutex_tid, NULL, mutex_thread, NULL); 47 pthread_create(&sleep_tid, NULL, sleep_thread, NULL); 48 pthread_kill(sleep_tid, SIGUSR1); 49 pthread_join(mutex_tid, NULL); 50 pthread_join(sleep_tid, NULL); 51 return 0; 52 }
运行结果:
为什么会死锁呢? 就是因为函数hold_mutex是不可重入的函数——其中使用了pthread_mutex互斥量。 当mutex_thread获得mutex时, sleep_thread就收到了信号, 再次调用就进入了hold_mutex。 结果始终无法拿到mutex, 信号处理函数无法返回, 正常的程序流程也无法继续, 这就造成了死锁。
我们可以调试一下看看具体卡在了哪里:
(1)看进程和线程
可以看出原来的主线程是22761,而分出来的2个线程分别为22762和22763
(2)看总体栈信息
(3)分别跟踪每个线程
22761卡在了pthread_join的地方,就是main函数中的pthread_join(mutex_tid, NULL);它其实等待的是22762里面释放锁的过程
由于在22762中全局变量signal_handler_exit为0且c为0,那么就一直在sleep那里搞了,根本就没法释放锁
由于22762中不能一直在循环sleep,那么信号处理函数对应的线程根本无法获取锁,那么就死锁了,由于有22762也是一直不能改变状态,所以还是释放不了锁,恶性循序也就死锁了
其实更详细的过程我们可以通过gdb去attach调试一下,这个以后有空了再分析吧……
2017-10-15 New update:
多次尝试后也有上面的情况发生,这是因为先进入了信号处理的线程,他会更改全局变量signal_handler_exit的值,这样的话另外一个线程进来因为循环条件不满足就不会一直循环sleep了,这就没有死锁情况的发生。
参考自:
http://blog.csdn.net/guaidaojidewo/article/details/20128989
http://www.cnblogs.com/lidabo/p/4523755.html
以上是关于PILE读书笔记_基础知识的主要内容,如果未能解决你的问题,请参考以下文章