Linux中为什么要随机函数栈的linux 栈起始地址址

阅读(123) | 评论(0) | 转发(0) |
上一篇:zfs当中的事务处理研究与探索
下一篇:zfs当中的事务处理(二)
相关热门文章
标准输入输出函数
Linux errno 错误对照表
在驱动模块初始化函数中实现设...
expect 总结(一)
3月13日外电头条:云服务颠覆...
承接自动化测试培训、外包、实...
Solaris PowerTOP 1.0 发布
For STKMonitor
项目小体会
不用学的汉字输入法 智能H3输...
这样配置的服务器能够承受8k并...
vm里的系统能够ping到nfs,但...
bind是否随机从两台master中读...
在win7下用cygwin搭建hadoop,...
Mysql的binglog日志能否分库备...
给主人留下些什么吧!~~
本文标题:
本页链接:新手园地& & & 硬件问题Linux系统管理Linux网络问题Linux环境编程Linux桌面系统国产LinuxBSD& & & BSD文档中心AIX& & & 新手入门& & & AIX文档中心& & & 资源下载& & & Power高级应用& & & IBM存储AS400Solaris& & & Solaris文档中心HP-UX& & & HP文档中心SCO UNIX& & & SCO文档中心互操作专区IRIXTru64 UNIXMac OS X门户网站运维集群和高可用服务器应用监控和防护虚拟化技术架构设计行业应用和管理服务器及硬件技术& & & 服务器资源下载云计算& & & 云计算文档中心& & & 云计算业界& & & 云计算资源下载存储备份& & & 存储文档中心& & & 存储业界& & & 存储资源下载& & & Symantec技术交流区安全技术网络技术& & & 网络技术文档中心C/C++& & & GUI编程& & & Functional编程内核源码& & & 内核问题移动开发& & & 移动开发技术资料ShellPerlJava& & & Java文档中心PHP& & & php文档中心Python& & & Python文档中心RubyCPU与编译器嵌入式开发驱动开发Web开发VoIP开发技术MySQL& & & MySQL文档中心SybaseOraclePostgreSQLDB2Informix数据仓库与数据挖掘NoSQL技术IT业界新闻与评论IT职业生涯& & & 猎头招聘IT图书与评论& & & CU技术图书大系& & & Linux书友会二手交易下载共享Linux文档专区IT培训与认证& & & 培训交流& & & 认证培训清茶斋投资理财运动地带快乐数码摄影& & & 摄影器材& & & 摄影比赛专区IT爱车族旅游天下站务交流版主会议室博客SNS站务交流区CU活动专区& & & Power活动专区& & & 拍卖交流区频道交流区
丰衣足食, 积分 975, 距离下一级还需 25 积分
论坛徽章:11
stack.c#include &stdio.h&
void func (int , long , char *);
int main (void)
{
& & func (18, 99999, &hello world&);& &
& & return 0;
}
void func (int a, long b, char *c)
{
& & printf (&a = %p\n&, &a);
& & printf (&b = %p\n&, &b);
& & printf (&c = %p\n&, &c);
}复制代码编译运行结果:a = 0x7fff743ce50c
b = 0x7fff743ce500
c = 0x7fff743ce4f8复制代码函数参数的入栈顺序不是从右到左吗,且栈是从栈底到栈顶(高地址到低地址)分配内存空间的,即先传入func()的参数应该为char *,然后是long,最后才是int
但这里的结果竟然显示入栈顺序是从左到右?
另外,在我的系统上,sizeof(int) 为4byte,sizeof(long)为8byte,sizeof(void *)为8byte,char * c的地址 减 long b的地址结果为8,0x7fff743ce500-0x7fff743ce4f8=8,一个char *的地址
但int a的地址 减 long b的地址 0x7fff743ce50c-0x7fff743ce500=12,这里却不是一个long的地址,为什么?
请各位指教一下,谢谢!
&&nbsp|&&nbsp&&nbsp|&&nbsp&&nbsp|&&nbsp&&nbsp|&&nbsp
小富即安, 积分 3575, 距离下一级还需 1425 积分
论坛徽章:2
the 3rd argument is a pointer, not a pointer points to a pointer.
丰衣足食, 积分 975, 距离下一级还需 25 积分
论坛徽章:11
敢问楼上说的和我问的有什么关系?请指教!
稍有积蓄, 积分 473, 距离下一级还需 27 积分
论坛徽章:0
亲,objdump看一下下不就完了
所有入参以func函数的帧指针fp偏移来获取
栈指针sp在上下跑,帧指针fp对函数保持固定{:3_189:}
总得来说,爱咋咋整,无所谓咯
丰衣足食, 积分 975, 距离下一级还需 25 积分
论坛徽章:11
lxyscls_cu
惭愧啊,objdump,gdb什么的诊断工具小弟还不会用,请问楼上怎样使用objdump解决本例中的问题?
因为最近在看stdarg的实现部分,以及printf()之类的,所以就开始关注函数参数的入栈顺序了。。。
大富大贵, 积分 18267, 距离下一级还需 1733 积分
论坛徽章:11
本帖最后由 zylthinking 于
15:07 编辑
superwujc 发表于
stack.c编译运行结果:函数参数的入栈顺序不是从右到左吗,且栈是从栈底到栈顶(高地址到低地址)分配内存空 ...
不明白, 至少不是64位的原因;
zylthinking@linux:~$ ./a.out
a = 0xbfc73c10
b = 0xbfc73c14
c = 0xbfc73c18
zylthinking@linux:~$ cat a.c
& & #include &stdio.h&
& & void func (int , long , char *);
& & int main (void)
& && &&&func (18, 99999, &hello world&);& &
& && &&&return 0;
& & void func (int a, long b, char *c)
& && &&&printf (&a = %p\n&, &a);
& && &&&printf (&b = %p\n&, &b);
& && &&&printf (&c = %p\n&, &c);
zylthinking@linux:~$ uname -a
Linux linux 3.5.0-22-generic #34-Ubuntu SMP Tue Jan 8 21:41:11 UTC
i686 i686 GNU/Linux
zhaoyulong@macos:/Users/zhaoyulong$ ./a.out
a = 0x7fff5f30cb5c
b = 0x7fff5f30cb50
c = 0x7fff5f30cb48
zhaoyulong@macos:/Users/zhaoyulong$ cat a.c
& & #include &stdio.h&
& & void func (int , long , char *);
& & int main (void)
& && &&&func (18, 99999, &hello world&);& &
& && &&&return 0;
& & void func (int a, long b, char *c)
& && &&&printf (&a = %p\n&, &a);
& && &&&printf (&b = %p\n&, &b);
& && &&&printf (&c = %p\n&, &c);
zhaoyulong@macos:/Users/zhaoyulong$ gcc -m32 a.c
zhaoyulong@macos:/Users/zhaoyulong$ ./a.out
a = 0xbff63bc0
b = 0xbff63bbc
c = 0xbff63bb8
zhaoyulong@macos:/Users/zhaoyulong$ uname -a
Darwin localhost 12.2.0 Darwin Kernel Version 12.2.0: Sat Aug 25 00:48:52 PDT 2012; root:xnu-~1/RELEASE_X86_64 x86_64
zhaoyulong@macos:/Users/zhaoyulong$ gcc -m32 -g a.c
zhaoyulong@macos:/Users/zhaoyulong$ gdb ./a.out
GNU gdb 6.3.50- (Apple version gdb-1822) (Sun Aug&&5 03:00:42 UTC 2012)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type &show copying& to see the conditions.
There is absolutely no warranty for GDB.&&Type &show warranty& for details.
This GDB was configured as &x86_64-apple-darwin&...Reading symbols for shared libraries .. done
(gdb) b main
Breakpoint 1 at 0x1ebc: file a.c, line 7.
Starting program: /Users/zhaoyulong/a.out
Reading symbols for shared libraries +......................... done
Breakpoint 1, main () at a.c:7
7& & & && && && &func (18, 99999, &hello world&);& &
(gdb) disassemble
Dump of assembler code for function main:
0x00001eb0 &main+0&:& & & & push& &%ebp
0x00001eb1 &main+1&:& & & & mov& & %esp,%ebp
0x00001eb3 &main+3&:& & & & sub& & $0x18,%esp
0x00001eb6 &main+6&:& & & & call& &0x1ebb &main+11&
0x00001ebb &main+11&:& & & & pop& & %eax
0x00001ebc &main+12&:& & & & lea& & 0xd1(%eax),%eax
0x00001ec2 &main+18&:& & & & movl& &$0x12,(%esp)
0x00001ec9 &main+25&:& & & & movl& &$0x(%esp)
0x00001ed1 &main+33&:& & & & mov& & %eax,0x8(%esp)
0x00001ed5 &main+37&:& & & & call& &0x1ef0 &func&
0x00001eda &main+42&:& & & & movl& &$0x0,-0x8(%ebp)
0x00001ee1 &main+49&:& & & & mov& & -0x8(%ebp),%eax
0x00001ee4 &main+52&:& & & & mov& & %eax,-0x4(%ebp)
0x00001ee7 &main+55&:& & & & mov& & -0x4(%ebp),%eax
0x00001eea &main+58&:& & & & add& & $0x18,%esp
0x00001eed &main+61&:& & & & pop& & %ebp
0x00001eee &main+62&:& & & & ret& &
End of assembler dump.
End of assembler dump.
The program is running.&&Exit anyway? (y or n) y
zhaoyulong@macos:/Users/zhaoyulong$ gcc --version
i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build )
Copyright (C) 2007 Free Software Foundation, Inc.
T see the source for copying conditions.&&There is NO
not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 他娘的貌似是 llvm-gcc 自己在搞鬼, 看楼主的输出和我的 mac 下输出一模一样的情况看, 八成用的也是mac os
稍有积蓄, 积分 473, 距离下一级还需 27 积分
论坛徽章:0
本帖最后由 lxyscls_cu 于
14:57 编辑
& & objdump -Sl xxx & log 当然编译的时候请加-g
关于你的问题——请参看《深入理解计算机系统》,有一章讲得很清楚,顺序还不是编译器说了算么,不需要纠结
小富即安, 积分 3192, 距离下一级还需 1808 积分
论坛徽章:4
貌似通过寄存器传递的那部分,是从左向右的~~~~~~~~~~
寄存器用完了,不得不通过memory传的话,就是从右向左了~~
小富即安, 积分 3192, 距离下一级还需 1808 积分
论坛徽章:4
我这边-O3优化出现更搞情况,呵呵,解释不通,算了~~~~~~
丰衣足食, 积分 975, 距离下一级还需 25 积分
论坛徽章:11
本帖最后由 superwujc 于
15:14 编辑
zylthinking
我的系统root@c-learning:/c-dev/datastructure# uname -a
Linux c-learning 2.6.32-5-amd64 #1 SMP Sun Sep 23 10:07:46 UTC
GNU/Linux
root@c-learning:/c-dev/datastructure#
root@c-learning:/c-dev/datastructure# gcc --version
gcc (Debian 4.4.5-8) 4.4.5
Copyright (C) 2010 Free Software Foundation, Inc.
T see the source for copying conditions.&&There is NO
not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.复制代码
北京皓辰网域网络信息技术有限公司. 版权所有 京ICP证:060528号 北京市公安局海淀分局网监中心备案编号:
广播电视节目制作经营许可证(京) 字第1234号
中国互联网协会会员&&联系我们:
感谢所有关心和支持过ChinaUnix的朋友们
转载本站内容请注明原作者名及出处Linux中为什么要随机函数栈的起始地址
Linux中为什么要随机函数栈的起始地址
  1. 如前文()所述,为了执行一个程序,首先do_execve建立数据结构,并将一些数据从用户空间拷贝到内核空间,然后调用search_binary_handler加载可执行文件映像。
int&do_execve(char&*&filename,&&
char&__user&*__user&*argv,&&
char&__user&*__user&*envp,&&
struct&pt_regs&*&regs)&&2. search_binary_handler()寻找对应的handler。对于elf 文件,即是load_elf_binary。
int&search_binary_handler(struct&linux_binprm&*bprm,struct&pt_regs&*regs)&&
3. load_elf_binary()读取可执行文件头文件信息,进行简单的一致性检测,分配用户模式的页表,设置栈的起始地址,加载可执行文件映像到内存;然后调用create_elf_tables(); 最后调用start_thread(),执行_start函数开始的代码。
staticint&load_elf_binary(struct&linux_binprm&*bprm,&struct&pt_regs&*regs)&&
  4. create_elf_tables()将参数指针,环境变量数组指针压入用户模式的栈。
create_elf_tables(struct&linux_binprm&*bprm,&struct&elfhdr&*exec,&&
&&&&&&&&&&&&&&&&unsigned&long&load_addr,&unsigned&long&interp_load_addr)&&值得注意的是,create_elf_tables()很可能会在压栈前调整栈指针。比如,在支持超线程的体系结构里面,通过随机化初始栈指针,可以减少进程间在L1上的竞争。如下所示,随机化初始栈指针的页内偏移量,并使得栈指针保持16字节对齐。
unsigned&long&arch_align_stack(unsigned&long&sp)&&
if&(!(current-&personality&&&ADDR_NO_RANDOMIZE)&&&&randomize_va_space)&&
&&&&&&&&&&&&&&&&sp&-=&get_random_int()&%&8192;&&
return&sp&&&~0&&
}&&为什么针对页内偏移呢?
  在某些体系结构中,首先要完成从逻辑地址到物理地址的转换,然后才能去cache中查找该物理地址是否已经在cache当中。这样,cache命中的代价较高。一种常用的技巧是,在L1中,逻辑地址索引-物理地址比较(virtually indexed, physically tagged)[1]。思路是,利用逻辑地址与物理地址的页内偏移一样的特点,用页内偏移进行索引,页号通过TLB转换成物理页号进行tag比较。这样,可以不经转换,就先索引,从而加快速度。这样,如果两个逻辑地址的块内偏移一样,它们索引的cache行也就一样,所以需要随机化页内偏移来减少L1的竞争。其缺点是,L1的set大小,不能超过页的大小。换言之:
  & & L1的大小 &= 相联度 * 块的大小 * 页的大小5.&start_thread(),执行_start函数开始的代码
start_thread(struct&pt_regs&*regs,&unsigned&long&new_ip,&unsigned&long&new_sp)&&
&&&&&&&&set_user_gs(regs,&0);&&
&&&&&&&&regs-&fs&&&&&&&&&&&&&&&&=&0;&&
&&&&&&&&set_fs(USER_DS);&&
&&&&&&&&regs-&ds&&&&&&&&&&&&&&&&=&__USER_DS;&&
&&&&&&&&regs-&es&&&&&&&&&&&&&&&&=&__USER_DS;&&
&&&&&&&&regs-&ss&&&&&&&&&&&&&&&&=&__USER_DS;&&
&&&&&&&&regs-&cs&&&&&&&&&&&&&&&&=&__USER_CS;&&
&&&&&&&&regs-&ip&&&&&&&&&&&&&&&&=&new_&&
&&&&&&&&regs-&sp&&&&&&&&&&&&&&&&=&new_&&
H3C认证Java认证Oracle认证
基础英语软考英语项目管理英语职场英语
.NETPowerBuilderWeb开发游戏开发Perl
二级模拟试题一级模拟试题一级考试经验四级考试资料
软件测试软件外包系统分析与建模敏捷开发
法律法规历年试题软考英语网络管理员系统架构设计师信息系统监理师
高级通信工程师考试大纲设备环境综合能力
路由技术网络存储无线网络网络设备
CPMP考试prince2认证项目范围管理项目配置管理项目管理案例项目经理项目干系人管理
职称考试题目
招生信息考研政治
网络安全安全设置工具使用手机安全
生物识别传感器物联网传输层物联网前沿技术物联网案例分析
Java核心技术J2ME教程
Linux系统管理Linux编程Linux安全AIX教程
Windows系统管理Windows教程Windows网络管理Windows故障
数据库开发Sybase数据库Informix数据库
&&&&&&&&&&&&&&&
希赛网 版权所有 & &&laokaddk 的BLOG
用户名:laokaddk
文章数:983
评论数:106
访问量:2382847
注册日期:
阅读量:5863
阅读量:12276
阅读量:382486
阅读量:1074258
51CTO推荐博文
如果把一个程序比作一个世界,那么程序的启动无疑就是&创世&。在本章里,我们将从程序的创世开始,接触到在程序背后另一类默默服务的团体。它们能够使得程序正常地启动,能够使得各种我们熟悉的函数发挥作用,它们就是应用程序的运行库。
读者可以免费到下glibc的源代码,在其中的子目录libc/csu里,有关于程序启动的代码。glibc的程序入口为_start(这个入口是由ld链接器默认的链接脚本所指定的,我们也可以通过相关参数设定自己的入口)。_start由汇编实现,并且和平台相关,下面可以单独看i386的_start实现:
650) this.width=650;" alt="linux:入口函数和程序初始化" src="//19767.jpg" width="500" height="304" />
   这里省略了一些不重要的代码,可以看到_start函数最终调用了名为__lib_start_main的函数。加粗部分的代码是对该函数的完整调用过程,其中开始的7个压栈指令用于给函数传递参数。在最开始的地方还有3条指令,它们的作用分别为:
  l xor %ebp, %ebp:这其实是让ebp寄存器清零。xor的用处是把后面的两个操作数异或,结果存储在第一个操作数里。这样做的目的表明当前是程序的最外层函数。
  ebp设为0正好可以体现出这个最外层函数的尊贵地位J。
  l pop %esi及mov %esp, %ecx:在调用_start前,装载器会把用户的参数和环境变量压入栈中,按照其压栈的方法,实际上栈顶的元素是argc,而接着其下就是argv和环境变量的数组。图11-1为此时的栈布局,其中虚线箭头是执行pop %esi之前的栈顶(%esp),而实线箭头是执行之后的栈顶(%esp)。
650) this.width=650;" alt="linux:入口函数和程序初始化" src="//19768.jpg" width="500" height="176" />
   pop %esi将argc存入了esi,而mov %esp、%ecx将栈顶地址(此时就是argv和环境变量(env)数组的起始地址)传给%ecx。现在%esi指向argc,%ecx指向argv及环境变量数组。
 综合以上分析,我们可以把_start改写为一段更具有可读性的伪代码:
  void _start()
  %ebp = 0;
  int argc = pop from stack
  char** argv =
  __libc_start_main( main, argc, argv, __libc_csu_init, __libc_csu_fini,
  edx, top of stack );
  其中argv除了指向参数表外,还隐含紧接着环境变量表。这个环境变量表要在__libc_start_main里从argv内提取出来。
  环境变量
  环境变量是存在于系统中的一些公用数据,任何程序都可以访问。通常来说,环境变量存储的都是一些系统的公共信息,例如系统搜索路径,当前OS版本等。环境变量的格式为key=value的字符串,C语言里可以使用getenv这个函数来获取环境变量信息。
  在里,可以直接在控制面板&系统&高级&环境变量查阅当前的环境变量,而在下,直接在命令行里输入export即可。
  实际执行代码的函数是__libc_start_main,由于代码很长,下面我们一段一段地看:
650) this.width=650;" alt="linux:入口函数和程序初始化" src="//19769.jpg" width="500" height="256" />
  这是__libc_start_main的函数头部,可见和_start函数里的调用一致,一共有7个参数,其中main由第一个参数传入,紧接着是argc和argv(这里称为ubp_av,因为其中还包含了环境变量表)。除了main的函数指针之外,外部还要传入3个函数指针,分别是:
  l init:main调用前的初始化工作。
  l fini:main结束后的收尾工作。
l rtld_fini:和动态加载有关的收尾工作,rtld是runtime loader的缩写。
  最后的stack_end标明了栈底的地址,即最高的栈地址。
  bounded pointer
  GCC支持bounded类型指针(bounded指针用__bounded关键字标出,若默认为bounded指针,则普通指针用__unbounded标出),这种指针占用3个指针的空间,在第一个空间里存储原指针的值,第二个空间里存储下限值,第三个空间里存储上限值。__ptrvalue、__ptrlow、__ptrhigh 分别返回这3个值,有了3个值以后,越界错误便很容易查出来了。并且要定义__BOUNDED_POINTERS__这个宏才有作用,否则这3个宏定义是空的。
  不过,尽管bounded指针看上去似乎很有用,但是这个功能却在2003年被去掉了。因此现在所有关于bounded指针的关键字其实都是一个空的宏。鉴于此,我们接下来在讨论libc代码时都默认不使用bounded指针(即不定义__BOUNDED_POINTERS__)。
  接下来的代码如下:
  char** ubp_ev = &ubp_av[argc + 1];
  INIT_ARGV_and_ENVIRON;
  __libc_stack_end = stack_
  INIT_ARGV_and_ENVIRON这个宏定义于libc/sysdeps/generic/bp-start.h,展开后本段代码变为:
  char** ubp_ev = &ubp_av[argc + 1];
  __environ = ubp_
  __libc_stack_end = stack_
  图11-2实际上就是我们根据从_start源代码分析得到的栈布局,让__environ指针指向原来紧跟在argv数组之后的环境变量数组。
650) this.width=650;" alt="linux:入口函数和程序初始化" src="//19771.jpg" width="500" height="188" />
图11-2中实线箭头代表ubp_av,而虚线箭头代表__environ。另外这段代码还将栈底地址存储在一个全局变量里,以留作它用。
  为什么要分两步赋值给__environ呢?这又是为了兼容bounded惹的祸。实际上,INIT_ARGV_and_ENVIRON根据bounded支持的情况有多个版本,以上仅仅是假定不支持bounded的版本。
  接下来有另一个宏:
  DL_SYSDEP_OSCHECK (__libc_fatal);
   这是用来检查的版本,宏的具体内容就不列出了。接下来的代码颇为繁杂,我们过滤掉大量信息之后,将一些关键的函数调用列出:
  __pthread_initialize_minimal();
  __cxa_atexit(rtld_fini, NULL, NULL);
  __libc_init_first (argc, argv, __environ);
  __cxa_atexit(fini, NULL, NULL);
  (*init)(argc, argv, __environ);
  这一部分进行了一连串的函数调用,注意到__cxa_atexit函数是glibc的内部函数,等同于atexit,用于将参数指定的函数在main结束之后调用。所以以参数传入的fini和rtld_fini均是用于main结束之后调用的。在__libc_start_main的末尾,关键的是这两行代码:
  result = main (argc, argv, __environ);
  exit (result);
  在最后,main函数终于被调用,并退出。然后我们来看看exit的实现:
650) this.width=650;" alt="linux:入口函数和程序初始化" src="//19773.jpg" width="500" height="218" />
   其中__exit_funcs是存储由__cxa_atexit和atexit注册的函数的链表,而这里的这个while循环则遍历该链表并逐个调用这些注册的函数,由于其中琐碎代码过多,这里就不具体列出了。最后的_exit函数由汇编实现,且与平台相关,下面列出i386的实现:
_start -& __libc_start_main -& exit -& _exit:
  _exit:
  movl 4(%esp), %ebx
  movl $__NR_exit, %eax
  int $0x80
  可见_exit的作用仅仅是调用了exit这个系统调用。也就是说,_exit调用后,进程就会直接结束。程序正常结束有两种情况,一种是main函数的正常返回,一种是程序中用exit退出。在__libc_start_main里我们可以看到,即使main返回了,exit也会被调用。exit是进程正常退出的必经之路,因此把调用用atexit注册的函数的任务交给exit来完成可以说万无一失。
  我们看到在_start和_exit的末尾都有一个hlt指令,这是作什么用的呢?在里,进程必须使用exit系统调用结束。一旦exit被调用,程序的运行就会终止,因此实际上_exit末尾的hlt不会执行,从而__libc_start_main永远不会返回,以至_start末尾的hlt指令也不会执行。_exit里的hlt指令是为了检测exit系统调用是否成功。如果失败,程序就不会终止,hlt指令就可以发挥作用强行把程序给停下来。而_start里的hlt的用处也是如此,但是为了预防某种没有调用exit(这里指的不是exit系统调用)就回到_start的情况(例如有人误删了__libc_main_start末尾的exit)。
  这是__libc_start_main的函数头部,可见和_start函数里的调用一致,一共有7个参数,其中main由第一个参数传入,紧接着是argc和argv(这里称为ubp_av,因为其中还包含了环境变量表)。除了main的函数指针之外,外部还要传入3个函数指针,分别是:
  l init:main调用前的初始化工作。
  l fini:main结束后的收尾工作。
  l rtld_fini:和动态加载有关的收尾工作,rtld是runtime loader的缩写。
  最后的stack_end标明了栈底的地址,即最高的栈地址。
  bounded pointer
  GCC支持bounded类型指针(bounded指针用__bounded关键字标出,若默认为bounded指针,则普通指针用__unbounded标出),这种指针占用3个指针的空间,在第一个空间里存储原指针的值,第二个空间里存储下限值,第三个空间里存储上限值。__ptrvalue、__ptrlow、__ptrhigh 分别返回这3个值,有了3个值以后,内存越界错误便很容易查出来了。并且要定义__BOUNDED_POINTERS__这个宏才有作用,否则这3个宏定义是空的。
  不过,尽管bounded指针看上去似乎很有用,但是这个功能却在2003年被去掉了。因此现在所有关于bounded指针的关键字其实都是一个空的宏。鉴于此,我们接下来在讨论libc代码时都默认不使用bounded指针(即不定义__BOUNDED_POINTERS__)。
  接下来的代码如下:
  char** ubp_ev = &ubp_av[argc + 1];
  INIT_ARGV_and_ENVIRON;
  __libc_stack_end = stack_
  INIT_ARGV_and_ENVIRON这个宏定义于libc/sysdeps/generic/bp-start.h,展开后本段代码变为:
  char** ubp_ev = &ubp_av[argc + 1];
  __environ = ubp_
  __libc_stack_end = stack_
  图11-2实际上就是我们根据从_start源代码分析得到的栈布局,让__environ指针指向原来紧跟在argv数组之后的环境变量数组。
  图11-2中实线箭头代表ubp_av,而虚线箭头代表__environ。另外这段代码还将栈底地址存储在一个全局变量里,以留作它用。
  为什么要分两步赋值给__environ呢?这又是为了兼容bounded惹的祸。实际上,INIT_ARGV_and_ENVIRON根据bounded支持的情况有多个版本,以上仅仅是假定不支持bounded的版本。
  接下来有另一个宏:
  DL_SYSDEP_OSCHECK (__libc_fatal);
这是用来检查操作系统的版本,宏的具体内容就不列出了。接下来的代码颇为繁杂,我们过滤掉大量信息之后,将一些关键的函数调用列出:
  __pthread_initialize_minimal();
  __cxa_atexit(rtld_fini, NULL, NULL);
  __libc_init_first (argc, argv, __environ);
  __cxa_atexit(fini, NULL, NULL);
  (*init)(argc, argv, __environ);
  这一部分进行了一连串的函数调用,注意到__cxa_atexit函数是glibc的内部函数,等同于atexit,用于将参数指定的函数在main结束之后调用。所以以参数传入的fini和rtld_fini均是用于main结束之后调用的。在__libc_start_main的末尾,关键的是这两行代码:
  result = main (argc, argv, __environ);
  exit (result);
  在最后,main函数终于被调用,并退出。然后我们来看看exit的实现:
  其中__exit_funcs是存储由__cxa_atexit和atexit注册的函数的链表,而这里的这个while循环则遍历该链表并逐个调用这些注册的函数,由于其中琐碎代码过多,这里就不具体列出了。最后的_exit函数由汇编实现,且与平台相关,下面列出i386的实现:
  _start -& __libc_start_main -& exit -& _exit:
  _exit:
  movl 4(%esp), %ebx
  movl $__NR_exit, %eax
  int $0x80
  可见_exit的作用仅仅是调用了exit这个系统调用。也就是说,_exit调用后,进程就会直接结束。程序正常结束有两种情况,一种是main函数的正常返回,一种是程序中用exit退出。在__libc_start_main里我们可以看到,即使main返回了,exit也会被调用。exit是进程正常退出的必经之路,因此把调用用atexit注册的函数的任务交给exit来完成可以说万无一失。
我们看到在_start和_exit的末尾都有一个hlt指令,这是作什么用的呢?在Linux里,进程必须使用exit系统调用结束。一旦exit被调用,程序的运行就会终止,因此实际上_exit末尾的hlt不会执行,从而__libc_start_main永远不会返回,以至_start末尾的hlt指令也不会执行。_exit里的hlt指令是为了检测exit系统调用是否成功。如果失败,程序就不会终止,hlt指令就可以发挥作用强行把程序给停下来。而_start里的hlt的用处也是如此,但是为了预防某种没有调用exit(这里指的不是exit系统调用)就回到_start的情况(例如有人误删了__libc_main_start末尾的exit)。
  MSVC CRT入口函数
  相信读者对glibc的入口函数已经有了一些了解。但可惜的是glibc的入口函数书写得不是非常直观。事实上,我们也没从glibc的入口函数了解到多少内容。为了从另一面看世界,我们再来看看Windows下的运行库的实现细节。下面是Microsoft Visual Studio 2003里crt0.c(位于VC安装目录的crt\src)的一部分。这里也删除了一些条件编译的代码,留下了比较重要的部分。MSVC的CRT默认的入口函数名为mainCRTStartup:
  被赋值的这些变量,是VC7里面预定义的一些全局变量,其中_osver和_winver表示操作系统的版本,_winmajor是主版本号,更具体的可以查阅MSDN。这段代码通过调用GetVersionExA(这是一个Windows API)来获得当前的操作系统版本信息,并且赋值给各个全局变量。
  为什么这里为posvi分配内存不使用malloc而使用alloca呢?是因为在程序的一开始堆还没有被初始化,而alloca是唯一可以不使用堆的动态分配机制。alloca可以在栈上分配任意大小的空间(只要栈的大小允许),并且在函数返回的时候会自动释放,就好像局部变量一样。
  由于没有初始化堆,所以很多事情没法做,当务之急是赶紧把堆先初始化了:
  if ( !_heap_init(0) )
  fast_error_exit(_RT_HEAPINIT);
  这里使用_heap_init函数对堆(heap)进行了初始化,如果堆初始化失败,那么程序就直接退出了。
这里是一个Windows的SEH的try-except块,里面做了什么呢?首先使用_ioinit函数初始化了I/O,接下来这段代码调用了一系列函数进行各种初始化,包括:
  l _setargv:初始化main函数的argv参数。
  l _setenv:设置环境变量。
  l _cinit:其他的C库设置。
  在最后,可以看到函数调用了main函数并获得了其返回值。try-except块的except部分是最后的清理阶段,如果try块里的代码发生异常,则在这里进行错误处理。最后退出并返回main的返回值。
  try-except块
  try-except块是Windows结构化异常处理机制SEH的一部分。try-except块的使用方法如下:
  __try {
  code 1
  __except(...) {
  code 2
  当code 1出现异常(段错误等)的时候,except部分的code 2会执行以异常处理。更为详细的信息请查阅MSDN。
  总结一下,这个mainCRTStartup的总体流程就是:
  (1)初始化和OS版本有关的全局变量。
  (2)初始化堆。
  (3)初始化I/O。
  (4)获取命令行参数和环境变量。
  (5)初始化C库的一些数据。
  (6)调用main并记录返回值。
  (7)检查错误并将main的返回值返回。
  事实上还是MSVC的入口函数的思路较为清晰。在第13章里,我们将仿照VC入口函数的思路实现一个Linux下的简易入口函数。
  Q:msvc的入口函数使用了alloca,它是如何实现的。
  A:alloca函数的特点是它能够动态地在栈上分配内存,在函数退出时如同局部变量一样自动释放。结合之前我们介绍的函数标准进入和退出指令序列就知道,函数退出时的退栈操作是直接将ESP的值赋为EBP的值。因此不管在函数的执行过程中ESP减少了多少,最后也能够成功地将函数执行时分配的所有栈空间回收。在这个基础上,alloca的实现就非常简单,仅仅是将ESP减少一定数值而已。
  Q:为什么MSVC的Win32程序的入口使用的是WinMain?
  A:WinMain和main一样,都不是程序的实际入口。MSVC的程序入口是同一段代码,但根据不同的编译参数被编译成了不同的版本。不同版本的入口函数在其中会调用不同名字的函数,包括main/wmain/WinMain/wWinMain等。
 11.1.3 运行库与I/O
  在了解了glibc和MSVC的入口函数的基本思路之后,让我们来深入了解各个初始化部分的具体实现。但在具体了解初始化之前,我们要先了解一个重要的概念:I/O。
  IO(或I/O)的全称是Input/Output,即输入和输出。对于计算机来说,I/O代表了计算机与外界的交互,交互的对象可以是人或其他设备(如图11-3所示)。
  而对于程序来说,I/O涵盖的范围还要宽广一些。一个程序的I/O指代了程序与外界的交互,包括文件、管道、网络、命令行、信号等。更广义地讲,I/O指代任何操作系统理解为&文件&的事务。许多操作系统,包括Linux和Windows,都将各种具有输入和输出概念的实体&&包括设备、磁盘文件、命令行等&&统称为文件,因此这里所说的文件是一个广义的概念。
  对于一个任意类型的文件,操作系统会提供一组操作函数,这包括打开文件、读文件、写文件、移动文件指针等,相信有编程经验的读者对此都不会陌生。有过C编程经验的读者应该知道,C语言文件操作是通过一个FILE结构的指针来进行的。fopen函数返回一个FILE结构的指针,而其他的函数如fwrite使用这个指针操作文件。使用文件的最简单代码如下:
  #include
  int main(int argc,char** argv)
  FILE* f = fopen( &test.dat&, &wb& );
  if( f == NULL )
  Return -1;
  fwrite( &123&, 3, 1, f );
  fclose(f);
  return 0;
  在操作系统层面上,文件操作也有类似于FILE的一个概念,在Linux里,这叫做文件描述符(File Descriptor),而在Windows里,叫做句柄(Handle)(以下在没有歧义的时候统称为句柄)。用户通过某个函数打开文件以获得句柄,此后用户操纵文件皆通过该句柄进行。
  设计这么一个句柄的原因在于句柄可以防止用户随意读写操作系统内核的文件对象。无论是Linux还是Windows,文件句柄总是和内核的文件对象相关联的,但如何关联细节用户并不可见。内核可以通过句柄来计算出内核里文件对象的地址,但此能力并不对用户开放。
下面举一个实际的例子,在Linux中,值为0、1、2的fd分别代表标准输入、标准输出和标准错误输出。在程序中打开文件得到的fd从3开始增长。fd具体是什么呢?在内核中,每一个进程都有一个私有的&打开文件表&,这个表是一个指针数组,每一个元素都指向一个内核的打开文件对象。而fd,就是这个表的下标。当用户打开一个文件时,内核会在内部生成一个打开文件对象,并在这个表里找到一个空项,让这一项指向生成的打开文件对象,并返回这一项的下标作为fd。由于这个表处于内核,并且用户无法访问到,因此用户即使拥有fd,也无法得到打开文件对象的地址,只能够通过系统提供的函数来操作。
  在C语言里,操纵文件的渠道则是FILE结构,不难想象,C语言中的FILE结构必定和fd有一对一的关系,每个FILE结构都会记录自己唯一对应的fd。
  FILE、fd、打开文件表和打开文件对象的关系如图11-4所示。
  图11-4中,内核指针p指向该进程的打开文件表,所以只要有fd,就可以用fd+p来得到打开文件表的某一项地址。stdin、stdout、stderr均是FILE结构的指针。
  对于Windows中的句柄,与Linux中的fd大同小异,不过Windows的句柄并不是打开文件表的下标,而是其下标经过某种线性变换之后的结果。
  在大致了解了I/O为何物之后,我们就能知道I/O初始化的职责是什么了。首先I/O初始化函数需要在用户空间中建立stdin、stdout、stderr及其对应的FILE结构,使得程序进入main之后可以直接使用printf、scanf等函数。
  11.1.4 MSVC CRT的入口函数初始化
  系统堆初始化
  MSVC的入口函数初始化主要包含两个部分,堆初始化和I/O初始化。MSVC的堆初始化由函数_heap_init完成,这个函数的定义位于heapinit.c,大致的代码如下(删去了64位系统的条件编译部分):
  mainCRTStartup -& _heap_init():
  HANDLE _crtheap = NULL;
  int _heap_init (int mtflag)
  if ( (_crtheap = HeapCreate( mtflag ? 0 : HEAP_NO_SERIALIZE,
  BYTES_PER_PAGE, 0 )) == NULL )
  return 0;
  return 1;
  在32位的编译环境下,MSVC的堆初始化过程出奇地简单,它仅仅调用了HeapCreate这个API创建了一个系统堆。因此不难想象,MSVC的malloc函数必然是调用了HeapAlloc这个API,将堆管理的过程直接交给了操作系统。
  I/O初始化
  I/O初始化相对于堆的初始化则要复杂很多。首先让我们来看看MSVC中,FILE结构的定义(FILE结构实际定义在C语言标准中并未指出,因此不同的版本可能有不同的实现):
  struct _iobuf {
  char *_
  char *_
  char *_
  typedef struct _iobuf FILE;
  这个FILE结构中最重要的一个字段是_file,_file是一个整数,通过_file可以访问到内部文件句柄表中的某一项。在Windows中,用户态使用句柄(Handle)来访问内核文件对象,句柄本身是一个32位的数据类型,在有些场合使用int来储存,有些场合使用指针来表示。
在MSVC的CRT中,已经打开的文件句柄的信息使用数据结构ioinfo来表示:
  typedef struct {
  intptr_
  在这个结构中,osfhnd字段即为打开文件的句柄,这里使用8字节整数类型intptr_t来存储。另外osfile的意义为文件的打开属性。而pipech字段则为用于管道的单字符缓冲,这里可以先忽略。osfile的值可由一系列值用按位或的方式得出:
  l FOPEN(0x01)句柄被打开。
  l FEOFLAG(0x02)已到达文件末尾。
  l FCRLF(0x04)在文本模式中,行缓冲已遇到回车符(见第11.2.2节)。
  l FPIPE(0x08)管道文件。
  l FNOINHERIT(0x10)句柄打开时具有属性_O_NOINHERIT(不遗传给子进程)。
  l FAPPEND(0x20)句柄打开时具有属性O_APPEND(在文件末尾追加数据)。
  l FDEV(0x40)设备文件。
  l FTEXT(0x80)文件以文本模式打开。
  在crt/src/ioinit.c中,有一个数组:
  ioinfo * __pioinfo[64]; // 等效于ioinfo __pioinfo[64][32];
  这就是用户态的打开文件表。这个表实际是一个二维数组,第二维的大小为32个ioinfo结构,因此该表总共可以容纳的元素总量为64 * 32 = 2048个句柄。此外_nhandle记录该表的实际元素个数。之所以使用指针数组而不是二维数组的原因是使用指针数组更加节省空间,而如果使用二维数组,则不论程序里打开了几个文件都必须始终消耗2048个ioinfo的空间。
  FILE结构中的_file的值,和此表的两个下标直接相关联。当我们要访问文件时,必须从FILE结构转换到操作系统的句柄。从一个FILE*结构得到文件句柄可以通过一个叫做_osfhnd的宏,当然这个宏是CRT内部使用的,并不推荐用户使用。_osfhnd的定义为:
  #define _osfhnd(i) ( _pioinfo(i)-&osfhnd )
  其中宏函数_pioinfo的定义是:
  #define _pioinfo(i) ( __pioinfo[(i) && 5] + ((i) & ((1 && 5) - 1)) )
  FILE结构的_file字段的意义可以从_pioinfo的定义里看出,通过_file得到打开文件表的下标变换为:
  FILE:_file的第5位到第10位是第一维坐标(共6位),_file的第0位到第4位是第二维坐标(共5位)。
  这样就可以通过简单的位运算来从FILE结构得到内部句柄。通过这我们可以看出,MSVC的I/O内部结构和之前介绍的Linux的结构有些不同,如图11-5所示。
  MSVC的I/O初始化就是要构造这个二维的打开文件表。MSVC的I/O初始化函数_ioinit定义于crt/src/ioinit.c中。首先,_ioinit函数初始化了__pioinfo数组的第一个二级数组:
  650) this.width=650;" style="border-bottom-color: #000000; border-top-color: #000000; border-right-color: #000000; border-left-color: #000000" border="1" alt="独家:入口函数和程序初始化" src=".cn/imagelist//ryyrbp4836mr.jpg" />
了这篇文章
类别:┆阅读(0)┆评论(0)

我要回帖

更多关于 栈地址 的文章

 

随机推荐