《程序员的自我修养》学习心得——第十一章 运行库

前言

这章主要想写的是入口函数和程序初始化的部分,在这张可以很清楚的看到,之前在做栈溢出的时候需要用到的知识点,比如_start_main_init 还有_csu_init等。虽然在溢出部分主要用的是函数的gadget,但是从执行流程来看,这一部分知识还是有必要了解一下的。这篇没有将整章内容提炼,如果需要了解其他知识的朋友可以自行阅读第十一章内容

如果这篇文章对你有帮助,就点个赞吧,有什么问题也可以在评论区留言Thanks♪(・ω・)ノ

一、入口函数和程序初始化

1、程序从main开始吗

我们可能在学校学习编程的时候,老师都会和我们讲程序是从main函数开始的。看起来这句话从编程的角度来看似乎没有什么问题,但是从执行流程上来看并不是这样。就比如说在前面做ret2scu的时候,我们所用到的gadget来自于scu_init,这个函数是先于main函数执行的,不只是这些初始化的函数,还有比如全局变量等,举个栗子:

#include <stdio.h>
#include <stdlib.h>
int a = 3;
int main(int argc, char* argv[]){
  int * p = (int *)malloc(sizeof(int));
  scanf("%d", p);
  printf("%d", a + *p);
  free(p);
}

从代码中可以看到,在程序刚执行到main的时候,全局变量的初始化过程已经结束了(a的值已经确定),main函数的两个参数(argc和argv)也被正确传进来。在这期间,栈和堆的初始化也已经完成了,可以看到代码中有scanf函数,证明需要使用键盘进行输入,那么一些系统I/O也被初始化了

从上面的例子可以看到,操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并负责调用main函数,这时候才可以在main函数里大胆的写各种代码:申请内存、使用系统调用、触发异常、访问I/O设备。

运行这些代码的函数称为入口函数或入口点(Entry Point),视平台的不同有不同的名字。程序的如酒店实际上是一个程序初始化和结束部分,往往是运行库的一部分。一个典型的程序运行步骤如下:

  • 操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数
  • 入口函数对运行库和运行环境进行初始化,包括堆、I/O、线程、全局变量构造等
  • 入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分
  • main函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程

2、入口函数如何实现

glibc的启动过程在不同的情况下差别很大,比如静态的glibc和动态的glibc的差别,glibc用于可执行文件和用于共享库的差别。这里只选取最简单的静态glibc用于可执行文件的时候作为例子。可以下载Linux下glibc的源代码,在其中的子目录libc/scu里,有关于程序启动的代码。glibc的程序入口为_start(这个入口是由ld链接器默认的链接脚本所指定的,可以通过相关参数设定自己的入口)。下面可以单独看i386的_start实现:

_start:
xor1 %ebp, %ebp
pop1 %esi
mov1 %esp, %ecx

push1 %esp
push1 %edx
push1 $__libc_scu_fini
push1 $__libc_scu_init
push1 %ecx
push1 %esi
push1 main
call __libc_start_main

省略了一些不重要的代码,也可以把程序扔进ida看完整的代码。从上面代码可以看到_start函数最终调用了名为__lib_start_main的函数,后面7个压栈指令用于给函数传递参数。在最开始的地方还有3条指令,作用分别为:

  • xor %ebp, %ebp:这其实是让ebp寄存器清零。xor的作用是把后面两个操作数异或,结果存储在第一个操作数里,这样做的目的表明当前是程序的最外层函数
  • pop %esi及mov %esp, %ecx:在调用_start前,装载器会把用户的参数和环境变量压入栈中,按照其压栈的方法,实际上栈顶元素是argc,而接着其下就是argv和环境变量的数组。下图为此时栈布局,虚线箭头是执行pop %esi之前的栈顶(%esp),实现箭头实质性之后的栈顶(%esp)在这里插入图片描述
    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 = top of stack;
  __libc_start_main(main, argc, argv, __libc_scu_init, __libc_scu_fini, edx, top of stack);
}

其中argv除了指向参数表外,还隐含紧接着环境变量表。这个环境变量表要在__libc_start_main里从argv内提取出来

实际执行代码的函数时__libc_start_main,由于代码很长,所以一段一段解释:

int __libc_start_main(
	int (*main) (int, char **, char**),
  int argc,
  char * __unbounded *__unbounded ubp_av,
  __typeof (main) init,
  void (*fini) (void),
  void (*rtld_fini) (void),
  void * __unbounded stack_end){
  #if __BOUNDED_POINTERS__
  		char **argv;
  #else
  		#define argv ubp_av
  #endif
  		int result;

这是__libc_start_main的函数头部,课件_start函数里的调用一直,一共有7个参数,其中main由第一个参数传入,紧接着agrc和argv(这里成为ubp_av,因为其中还包含了环境变量表)。除了main的函数指针外,外部还要传入3个函数指针:

  • init:main调用前的初始化工作
  • fini:main结束后的收尾工作
  • rtld_fini:和动态加载有关的收尾工作,rtld是runtime loader的缩写。最后的stack_end表明了栈底的地址,即最高的栈地址

接下来的代码如下:

char ** ubp_ev = &ubp_av[argc + 1];
INIT_ARGV_AND_ENVIRON;
__libc_stack_end = stack_end;

INIT_ARGV_AND_ENVIRON这个宏定义域libc/sysdeps/generic/bp-start.h展开后本代码变为:

char ** ubp_ev = &ubp_av[argc + 1];
__environ = ubp_ev;
__libc_stack_end = stack_end;

在这里插入图片描述上图就是根据从_start源代码分析得到的栈布局,让__environ指针指向原来紧跟在argv数组之后的环境变量数组。上图实现箭头代表ubp_av,而虚线箭头代表__environ。另外这段代码还将栈底地址存储在一个全局变量里,以留作它用

接下来有另一个宏:

DL_SYSDEP_OSCHECK (__libc_fatal);

这是用来检查操作系统版本。接下来的代码过滤掉大量信息后,将一些关键的函数调用列出:

__pyhread_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的内部函数,等同于atexut,用于将参数指定的函数在main结束之后调用,所以以参数传入fini和rtlf_fini均是用于main结束之后调用的。在__libc_start_main的末尾,关键的是这两行代码:

	result = main(argc, argv, __environ);
	exit (result);
}

在最后,main函数终于被调用,并推出

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_41202237/article/details/107174036