linux内核使用什么汇编语言,Linux内核设计(第一周)——从汇编语言出发理解计算机工作原理(示例代码)...

Linux内核设计(第一周)——从汇编语言出发理解计算机工作原理

计算机工作原理

汇编指令

C语言代码汇编分析

by苏正生

原创作品转载请注明出处

《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

从2月22日起,本学期的linux课程开始了。通过这两天的学习,觉得孟宁老师讲的真不错,条理清晰,举例适当。本周从计算机工作原理出发,回顾了冯诺依曼计算机结构,也回顾了汇编寄存器、汇编指令、C语言程序的汇编分析技巧,很是受用。

一.知识点回顾

1.冯诺依曼理论的要点是:数字计算机的数制采用二进制;计算机应该按照程序顺序执行。

2.以Intel 8086和8088为例有十四个16位寄存器,比如AX, BX, CX, DX到了32位处理器时代,相对于16位处理器进行了扩展,在16位的寄存器基础上加上E前缀,比如AX变成了EAX,在后来,AMD出了64位处理器,采用的R前缀。

3.汇编指令在32位机器中都以l结尾,AT&T格式的汇编指令是“源操作数在前,目的操作数在后”,而intel格式是反过来的,即如下:

AT&T格式:movl %eax, %edx

Intel格式:mov edx, eax

二、实验过程

1.在实验楼Linux系统实验平台编写c代码:

int g(int x){

return x + 3;

}

int f(int x){

return g(x);

}

int main(void){

return f(8) + 1;

}

30b0fd7bb7f747cbb6f2252fb826cf14.jpg

2.反编译

在实验楼平台下,使用 gcc -S -o main.s main.c -m32将它反汇编成main.s。注意,我们所用的实验平台是X86-64的操作系统,所以为了产生32位的汇编代码,我使用了-m32选项让它生成32位汇编指令

生成如下汇编代码:.file "main.c"

.text

.globl g

.type g, @function

g:

.LFB0:

.cfi_startproc

pushl %ebp

.cfi_def_cfa_offset 8

.cfi_offset 5, -8

movl %esp, %ebp

.cfi_def_cfa_register 5

movl 8(%ebp), %eax

addl $3, %eax

popl %ebp

.cfi_restore 5

.cfi_def_cfa 4, 4

ret

.cfi_endproc

.LFE0:

.size g, .-g

.globl f

.type f, @function

f:

.LFB1:

.cfi_startproc

pushl %ebp

.cfi_def_cfa_offset 8

.cfi_offset 5, -8

movl %esp, %ebp

.cfi_def_cfa_register 5

subl $4, %esp

movl 8(%ebp), %eax

movl %eax, (%esp)

call g

leave

.cfi_restore 5

.cfi_def_cfa 4, 4

ret

.cfi_endproc

.LFE1:

.size f, .-f

.globl main

.type main, @function

main:

.LFB2:

.cfi_startproc

pushl %ebp

.cfi_def_cfa_offset 8

.cfi_offset 5, -8

movl %esp, %ebp

.cfi_def_cfa_register 5

subl $4, %esp

movl $8, (%esp)

call f

addl $1, %eax

leave

.cfi_restore 5

.cfi_def_cfa 4, 4

ret

.cfi_endproc

.LFE2:

.size main, .-main

.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"

.section .note.GNU-stack,"",@progbits

323f3aa61b1445bcbb5889450953d03a.jpg

代码中有许多以.开头的代码行,属于链接时候的辅助信息,在实际中不会执行,把它删除,得到下列的代码就是纯汇编代码了:

g:

pushl %ebp

movl %esp, %ebp

movl 8(%ebp), %eax

addl $3, %eax

popl %ebp

ret

f:

pushl %ebp

movl %esp, %ebp

subl $4, %esp

movl 8(%ebp), %eax

movl %eax, (%esp)

call g

leave

ret

main:

pushl %ebp

movl %esp, %ebp

subl $4, %esp

movl $8, (%esp)

call f

addl $1, %eax

leave

ret

e7a162d9c57b4122bf38dab6309c96e7.jpg

三.汇编代码分析

经过观察,我们可以看出,每一个函数基本上都有一个几乎相同的汇编格式:

函数名:

pushl %ebp

movl %esp, %ebp

+函数中间过程

leave(或者popl %ebp)

ret

【注意】leave和下面代码等价

movl %ebp, %esp

popl %ebp

enter和下面代码等价

pushl %ebp

movl %esp, %ebp

1.函数执行

通过查阅资料,我们知道在计算机内部执行代码的时候,每当调用一个函数的时候,函数总是先把当前的栈底指针压入堆栈,然后把栈底指针移动到当前的栈顶,这样子做,相当于在旧的栈上新起了一个栈。然后在新栈上执行函数。

当函数执行结束的时候,如果堆栈有变化,我们可以用movl %ebp,%esp来恢复堆栈。如果函数结束后,堆栈没有变化,那么这句话就可以不要。

函数调用结束后,就要使用ret返回到调用它的函数。同时,我们还需要回复栈底指针,以便于函数返回值的传递。于是popl %ebp。通常相对叫简洁的汇编代码中,会用leave来代替刚才所用的两句话。

2.函数调用

函数执行一定得是有函数调用了。

pushl $2333

call f

addl $4, %esp

这是调用f(2333)函数的过程。

我们可以看到,我们把2333压栈,然后调用了f函数。

等到ret后,返回了现在的call的下一行汇编代码。这时候,esp和ebp是一个值,所以这以后如果压栈的时候,会覆盖了栈底指针,把esp往栈顶上移动1个单位也就是4个字节,这时候就完美解决了调用后的问题,才是真正调用完成了。

3.函数参数取得

这时候,得回头看一下f函数了。这时候,我们发现它用了

pushl 8(%ebp)

call g

addl $4, %esp

它把增加了8个字节的地址压栈了,然后调用了g函数。

分析一下为什么是8个字节,我们可以用sizeof关键字来测试得到int占4个字节……所以,它却加了8个字节取值,那么必然是有什么怪东西又入栈了。pushl %ebp是每次函数执行的时候使用的,就是ebp寄存器还占用了4个字节,如果是32位芯片,寄存器(32位=8位/字节\times 4字节)。

所以,又发现了ebp寄存器的一个好处,能够让我们方便取得函数的参数……否则后面再去参数,栈位置变了好多,就不方便了。

4.图解

(1)main函数

3e52a710304642548fa72b5b4d0c1eaf.jpg

此时ebp入栈,将esp的值赋给ebp,相当于一个旧的栈底指针。

然后esp-4,在栈中向下一个空格,然后在此位置放入8.

41ab6c141ebf4d57b165767946de8e60.jpg

然后此时调用f函数。call F ,函数调用指令,首先把当前eip的值[当前eip指向第四条指令,即movl $8, %esp]入栈,然后跳转到F函数的第一条指令开始执行。

此时栈中的情况如下如所示:

4fb70e852ddb40d7b1a3649990361d43.jpg

(2)f函数

这里前条指令和main函数的头两条指令作用相同,保存当前栈环境,为F函数开辟新的栈空间。然后将esp的值减4,跳到下一格。

pushl 8(%ebp),该指令把当前ebp中的数值加8后作为内存地址,并把该内存地址指向的内存空间内的数值"8"放入栈中。其实就是把调用函数是传入的参数入栈。

然后将eax中的值——8传给当前esp位置,相当于返回值。

此时,调用函数g。call g,函数调用指令,当前eip入栈后,跳转到G函数的第一条指令执行

92df956844864f2aaca2ee2d1e290fdb.jpg

(3)g函数

g函数和之前的f函数基本一致。

popl %ebp,从栈中获取旧的esp值,并放入ebp寄存器。[这里之所以没有再加上一条movl %ebp, %esp是因为函数中esp的值并没有改变,依然指向存放旧esp值的内存空间]

ret 等价于pop eip,从当前栈顶,即esp所指内存处获取值,作为eip,然后跳转到eip中存放的地址继续执行。

到这里,函数G已经返回,其返回值存储在eax寄存器中,即返回值为11

6b3f9be022854480aaf6e0346b9d3248.jpg

(4)返回到函数F中

740ed5195bca46c487ea59a603fb5c07.jpg

(4)返回到main中

1 ...

2 leave

3 ret

leave,等价于 如下两条指令

movl %ebp, %esp

pop %ebp

即函数结语,释放F函数使用的栈空间,此时栈中情况如图:

aa5db08208b940dcb615c1bcd70ce932.jpg

再接着是ret指令,该指令执行后,函数F返回,程序回到main函数继续执行

此时eax中存放的是函数exF的返回值,即11

回到main函数继续执行

1 ...

2 addl $1, %eax

3 leave

4 ret

addl $1, %eax 此时eax中的值是main函数调用函数F的得到的返回值,即11,本条指令将eax中的值加2后放回eax,执行后eax中的值为12

ret main函数返回

总结

通过本次试验,我们对于计算机对于程序的执行有了一些新的认识。计算机每次都是各种取指针执行,在程序中各种跳转。在函数执行前要enter,函数执行后要leave(如果没有改变esp就可以省去把ebp赋值给esp的步骤了),ret函数取值可以靠ebp很方便做到,函数调用结束后要记住恢复堆栈指针(esp)。当然还有很多不足,有一些问题都是通过查阅互联网来分析和理解的,难免有不正确的地方,希望之后和老师沟通和确认,也希望大家来指正。

参考资料:

1.七种寻址方式(直接寻址方式) - 李龙江 - 博客园

http://www.cnblogs.com/lilongjiang/archive/2011/06/14/2080551.html

2.汇编基础知识 - [C/C++] - 杨德龙的专栏 - 博客频道 - CSDN.NET

http://blog.csdn.net/yangdelong/article/details/2594660

3.Linux内核分析 - 网易云课堂

http://mooc.study.163.com/learn/USTC-1000029000?tid=2001214000#/learn/content

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值