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命令来分析他的执行过程。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main( )
{
int fd ;
int i = ;
fd = open( “/tmp/foo”, O_RDONLY ) ;
if ( fd < )
i=;
else
i=;
return i;
}
过程如下:
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等执行流程。 所以, 可重入函数一定是线程安全的, 而线程安全函数则不一定是可重入函数。
比如下面的代码就会造成死锁:
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h> static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static const char * const caller[] = {"mutex_thread", "signal handler"};
static pthread_t mutex_tid;
static pthread_t sleep_tid;
static volatile int signal_handler_exit = ; static void hold_mutex(int c)
{
printf("enter hold_mutex [caller %s]\n", caller[c]);
pthread_mutex_lock(&mutex);
/* 这里的循环是为了保证锁不会在信号处理函数退出前被释放掉
*/
while (!signal_handler_exit && c != ) {
sleep();
}
pthread_mutex_unlock(&mutex);
printf("leave hold_mutex [caller %s]\n", caller[c]);
} static void *mutex_thread(void *arg)
{
hold_mutex();
} static void *sleep_thread(void *arg)
{
sleep();
} static void signal_handler(int signum)
{
hold_mutex();
signal_handler_exit = ;
} int main()
{
signal(SIGUSR1, signal_handler);
pthread_create(&mutex_tid, NULL, mutex_thread, NULL);
pthread_create(&sleep_tid, NULL, sleep_thread, NULL);
pthread_kill(sleep_tid, SIGUSR1);
pthread_join(mutex_tid, NULL);
pthread_join(sleep_tid, NULL);
return ;
}
运行结果:
为什么会死锁呢? 就是因为函数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读书笔记_基础知识的更多相关文章
- PILE读书笔记_进程环境
进程是操作系统运行程序的一个实例, 也是操作系统分配资源的单位. 在Linux环境中, 每个进程都有独立的进程空间, 以便对不同的进程进行隔离, 使之不会互相影响. atexit函数 #include ...
- PILE读书笔记_标准I/O
在学习和分析标准I/O库的同时, 可以重点与Linux的I/O系统调用进行比较. stdin. stdout和stderr都是FILE类型的文件指针, 是由C库静态定义的, 直接与文件描述符0. 1和 ...
- PILE读书笔记_文件I/O
open函数 int open(const char *pathname, int flags, mode_t mode); 参数说明: (1)pathname: 表示要打开的文件路径 (2)flag ...
- UNIX读书笔记----UNIX基础知识
UNIX体系结构: 从严格意义上讲,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境.我们通常将这种软件称为内核(Kernel),因为他相对较小,而且位于环境的核心.图片显示了UNI ...
- MyBatis:学习笔记(1)——基础知识
MyBatis:学习笔记(1)--基础知识 引入MyBatis JDBC编程的问题及解决设想 ☐ 数据库连接使用时创建,不使用时就释放,频繁开启和关闭,造成数据库资源浪费,影响数据库性能. ☐ 使用数 ...
- C#学习笔记(基础知识回顾)之值类型与引用类型转换(装箱和拆箱)
一:值类型和引用类型的含义参考前一篇文章 C#学习笔记(基础知识回顾)之值类型和引用类型 1.1,C#数据类型分为在栈上分配内存的值类型和在托管堆上分配内存的引用类型.如果int只不过是栈上的一个4字 ...
- C#学习笔记(基础知识回顾)之值传递和引用传递
一:要了解值传递和引用传递,先要知道这两种类型含义,可以参考上一篇 C#学习笔记(基础知识回顾)之值类型和引用类型 二:给方法传递参数分为值传递和引用传递. 2.1在变量通过引用传递给方法时,被调用的 ...
- C#学习笔记(基础知识回顾)之值类型和引用类型
一:C#把数据类型分为值类型和引用类型 1.1:从概念上来看,其区别是值类型直接存储值,而引用类型存储对值的引用. 1.2:这两种类型在内存的不同地方,值类型存储在堆栈中,而引用类型存储在托管对上.存 ...
- Quartz学习笔记:基础知识
Quartz学习笔记:基础知识 引入Quartz 关于任务调度 关于任务调度,Java.util.Timer是最简单的一种实现任务调度的方法,简单的使用如下: import java.util.Tim ...
随机推荐
- u-boot中添加mtdparts支持以及Linux的分区设置
简介 作者:彭东林 邮箱:pengdonglin137@163.com u-boot版本:u-boot-2015.04 Linux版本:Linux-3.14 硬件平台:tq2440, 内存:64M ...
- jQuery用noConflict代替$
js框架很多的情况下,很容易出现冲突,建议使用noConflict代替$ //消除$对jquery缩写 $.noConflict(); //使用了noConflict后,用$就会无效,应用jQuery ...
- css活用,半星星的效果
1.首先下载要用到星星字体 http://www.w3cplus.com/w3cplusDemo/demos/webFontIcon.html 2.css .cleanfloat::after{dis ...
- PostgreSQL on Linux 最佳部署手册
安装常用包 # yum -y install coreutils glib2 lrzsz mpstat dstat sysstat e4fsprogs xfsprogs ntp readline-de ...
- MariaDB数据库管理系统
MYSQL数据库管理系统被Oracle公司收购后从开源换向到了封闭,导致许多Linux发行版选择了MariaDB. MYSQL是一款大家都非常熟知的数据库管理系统,技术成熟.配置简单.开源免费并且 ...
- Solr6 Suggest(智能提示)
1.介绍 Solr从1.4开始便提供了检查建议,检索建议目前是各大搜索的标配应用,主要作用是避免用户输入错误的搜索词,同时将用户引导到相应的关键词搜索上.通常,我们将其称为搜索联想. 其效果如图所示. ...
- javascript通过url向jsp页面传递中文参数乱码解决方法
解决方法:在传递参数前将中文参数进行两次编码,jsp页面获取参数后对中文参数进行一次解码,中文参数就不会变为乱码了! 参考例子: <%@ page language="java&quo ...
- SVN服务的部署及使用
环境说明 系统版本 CentOS 7.2 x86_64 SVN是Subversion的简称,是一个开放源代码的版本控制系统,相较于RCS.CVS,它采用了分支管理系统,它的设计目标就是 ...
- ubuntu下wine操作usb串口
呃,换成ubuntu后想玩下文曲星,貌似没有linux下的下载工具,只好用wine. 用的是ch340的usb转串口,不得不说ubuntu果然集大成,这些驱动都有了,用minicom设置设备为/dev ...
- 搭建svnserve并创建提交钩子
之前做过很多这个过程了,但每次总有些地方不记得要查资料,现在顺手记录一下,以后好查. 安装svn apt-get install subversion 创建代码仓库 svnadmin create & ...