《Linux内核原理与分析》第二周作业
这一周学习了MOOCLinux内核分析的第一讲,计算机是如何工作的?由于本科对相关知识的不熟悉,所以感觉有的知识理解起来了有一定的难度,不过多查查资料,看看别人的解答,慢慢的也就理解了,最终形成自己的知识脉络。
实验分析
先创建文件,通过vim将C代码写到文件中去,如图。
再编译成可执行程序和反编译成汇编代码。为什么反编译是这个代码呢?
gcc -S -o main.s main.c
原来gcc命令中 -S 参数表示仅仅汇编而不进行编译及链接,也就是将源代码翻译成汇编指令。-o filename 指明输出文件名。这里表示将生成的汇编代码保存到main.s中去。汇编并整理之后的代码如图所示:
这个代码跟我之前学的汇编代码有稍许的不一样,查了一下原因,才知道之前那个是Windows平台下的汇编代码,这个是linux下的汇编代码,大同小异,稍微查一下资料并仔细阅读还是能读懂,比如%表示寄存器里面的内容。本函数只有一个参数,所以在汇编上体现出来的就是直接的一个入栈操作,那么如果是两个或者以上的参数呢,应该先压栈哪个?带着这样的疑问我又去查资料,得知,参数的入栈顺序时候从右往左的,也就是最右边的参数是最先入栈的。为何参数入栈的顺序是从右往左而不是从左往右呢?
要回答这个问题,就不得不谈一谈printf()函数,printf函数的原型是:printf(const char* format,…),没错,它是一个不定参函数,那么我们在实际使用中是怎么样知道它的参数个数呢?这就要靠format了,编译器通过format中的%占位符的个数来确定参数的个数。现在我们假设参数的压栈顺序是从左到右的,这时,函数调用的时候,format最先进栈,之后是各个参数进栈,最后pc进栈,此时,由于format先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到format,而要找到format,必须要知道参数的个数,这样就陷入了一个无法求解的死循环了!!
而如果把参数从右到左压栈,情况又是怎么样的?函数调用时,先把若干个参数都压入栈中,再压format,最后压返回地址,压EBP,这样一来,栈顶指针加2便找到了format,通过format中的%占位符,取得后面参数的个数,从而正确取得所有参数。
下面开始代码的具体分析:
首先从main函数开始,因为在main函数开始前,程序已经做了一堆准备工作,所以指令肯定不是从这才开始的,前面已经执行了很多的指令,所以先将原先的ebp压栈,ebp压栈之后,esp-=4,并在esp新指向的内存地址存入ebp,形成一个调用main函数产生的新的相对的栈,然后ebp指向当前栈顶指针esp所指向的位置。
subl $4,%esp
movl $10,(%esp)
表示将esp自减4,即向下移动四个字节,然后将立即数10放入到esp目前所保存的地址中的内存单元中去,这两行代码亦可以用
pushl $10
来代替。接下来要调用f函数了,本来不调用f的话,程序会按照顺序执行下去,也就是执行addl $3,%eax
这条命令,当调用f的时候,程序便更改了EIP的值,改为跳转到f的地址,所以在这之前,要将addl $3,%eax
这条语句的地址也就是返回地址压栈,然后进入f函数中去。
进入f函数之后同样会形成一个新的相对的栈,所以首先是将ebp压栈,保存之前的ebp的值,ebp再被esp赋值之后,再指向当前的栈顶。这个时候esp-4,即下移一个内存单元,movl 8(%ebp),eax
这句命令的意思是将当前ebp所指向的内存地址+8之后的内存单元所存的内容取出来放到eax中去,那么当前ebp+8之后是什么呢。由之前的分析可得到,当前ebp+4是得到f函数的返回地址,那么+8之后便是立即数10,所以这一步是将10取出来放到eax中去。由于刚刚栈顶指针esp只是做下移操作,并没有存放任何东西,所以这里有一句movl %eax,(%esp)
,将eax中的值即10放到当前esp所指向的内存单元中去,然后调用g函数。同样,先将g函数的返回地址压栈,形成一个新的相对的栈之后,ebp压栈(esp-4并存放ebp),ebp指向esp当前所指向的位置。这里又有一句movl 8(%ebp),eax
,一样的,取出上面第二个单元格中的10出来放到eax中,eax再+5,再把当前的栈顶指针所指向的内存单元的值(即之前存放的ebp的值)取出来放到ebp中去,再返回,到函数之前的f中应该执行的位置(返回其实也对应着一个将EIP压栈,改变EIP值的操作,这里一般省略了),这里对g函数的分析就对应g函数中的c语言代码:return x+5;同样g调用完了返回到f函数之后,f也开始准备清理工作然后返回了(leave指令相当于一直清栈的指令,不过当然清当前这个相对的栈),最后回到主函数,将EAX中的值+3,由于之前的指令之后的eax中保存的是15,所以加3之后eax是18了,整个main函数也就差不多分析结束了,剩下的就是程序自己之后的一些工作了。
遇到的问题及解决方案
- 关于堆栈的问题:对计算机中的堆栈的知识感到陌生,这一块完全需要自己去补补。
通过上网查询资料,了解到了,栈的生长跟内存地址的生长刚好相反,栈是由内存高地址向内存低地址的方向生长的,所以每进行一个入栈操作的时候,栈顶指针所指向的内存地址自减4(为什么是自减4呢?因为一个内存单元是四个字节,所以当进行一个压栈操作的时候,就把指针自减4个字节,然后再把数据放进去)。 - 关于各种寄存器的问题:梳理一遍比较重要的几个寄存器的作用
以32位机为例,cpu中有一个很关键的寄存器叫EIP,它保存的是下一条待执行指令的地址,然后从相应的内存地址中找到指令并执行,再指向下一个内存地址。EIP可以被call ret jump 条件跳转等间接修改,但不能被程序员直接修改。除此之外还有EAX,EBX,ECX,EDX,ESP,EBP。关于ESP,EBP一直都没有理解的很透彻,不知道这两个寄存器各自的作用,通过这一次清楚的知道了,每当发生一个函数(包括main函数)调用的时候,都会产生一个新的相对的栈(在这之前会把返回地址压栈,原先的EBP压栈),然后这个新的相对的栈中,EBP指向栈底,ESP指向栈顶,当一个东西要进行入栈操作,ESP先自减4,再把要入栈的东西放到以当前ESP所指向的地址为其实的内存单元中去。下面一段代码通过自己的理解给出注释
//假设执行函数前堆栈指针ESP为NN
push para2 ;参数2入栈, ESP -= 4h , ESP = NN - 4h
push para1 ;参数1入栈, ESP -= 4h , ESP = NN - 8h
call test ;压入返回地址 ESP -= 4h, ESP = NN - 0Ch
;//进入test函数内
test
{
push ebp ;保护先前EBP指针,EBP入栈,ESP-=4h,ESP=NN - 10h
mov ebp, esp ;设置EBP指针指向栈顶 NN-10h
mov eax, dword ptr [ebp+0ch] ;ebp+0ch为NN-4h,即参数2的位置
mov ebx, dword ptr [ebp+08h] ;ebp+08h为NN-8h,即参数1的位置
sub esp, 8 ;局部变量所占空间ESP-=8, ESP = NN-18h
...
add esp, 8 ;释放局部变量, ESP+=8, ESP = NN-10h
pop ebp ;出栈,恢复EBP, ESP+=4, ESP = NN-0Ch
ret 8 ;ret返回,弹出返回地址,ESP+=4, ESP=NN-08h, 后面加操作数8为平衡堆栈,ESP+=8,ESP=NN, 恢复进入函数前的堆栈.
}
所以,ESP就是一直指向栈顶的指针,而EBP只是存取某时刻的栈顶指针,以方便对栈的操作,如获取函数参数、局部变量等。
3. 书上讲的内核压缩默认以gzip和bzip2的形式发布,但是我下载下来的却是以tar.xz为后缀名的压缩包(可能是书比较老的缘故吧),于是上网查询这形式的压缩包怎么解压,但是既然学到这,就得去了解.xz是怎么产生的。于是深入的了解了下,xz 是一个使用 LZMA压缩算法的无损数据压缩文件格式。一般可以使用tar -xvf filename.tar.xz
来解压。
总结
通过本章的学习,我理解了很多关于计算机工作原理的知识。以前一直不知道计算机怎么运作起来的,CPU,内存他们有什么联系。现在知道了,CPU中所执行的每一条指令都是从内存(CPU和内存之间通过总线连接)中读取的,而内存又可以看成是由一连串连续的地址的储存单元组成的,每个单元要么存放数据要么存放指令,所以计算机就反复通过cpu读取指令,执行指令,指向下一个内存地址的模式让我们computer运行起来啦。