初始状态
main:
标号开始(17)[注:括号内的数字为vim中汇编代码所在行号]
第一句(18):pushl %ebp
pushl做的工作是:先在栈内增长4字节(32位机),即esp偏移4个字节。然后再把ebp的内容存到栈顶。[注:虽是增长,但实际却是从高地址到低地址的过程,即栈底所在为高地址,栈顶从高地址向低地址增长]
该语句可以用以下两条语句表示:
subl $4, %esp
movl %ebp, (%esp)
效果如下图:
第二句(19):movl %esp, %ebp
使得ebp指向esp指向的位置,效果如下图:
第三句(20):subl $4, %esp
栈顶向下(低地址)增长,或者说偏移4个字节,效果如下图:
第四句(21):movl $9, (%esp)
将一个立即数9存入栈顶,效果如下图:
第五句(22):call f
调用f(),call的工作是将当前“call
f”的下一条指令的eip存入栈中(根据代码显示应为23行的”addl $6,
%eax”地址作为eip),并将标号f所在的地址视作当前指令的地址,该指令等同于以下指令:
pushl %eip
movl $0x****, %eip
可进一步扩展为:
subl $4, %esp
movl %eip, (%esp)
movl $0x****, %eip
效果如下图:
标号f开始(8)
第一句(9):pushl %ebp
同上,栈顶增长4字节,然后再把ebp的内容存到栈顶,效果如下图:
[注:ebp(1)中的“1”非汇编代码中的行号,而是栈的相对标号]
第二句(10):movl %esp, %ebp
使得ebp指向esp指向的位置,效果如下图
第三句(11):subl $4, %esp
栈顶向下(低地址)增长,或者说偏移4个字节,效果如下图:
第四句(12):movl 8(%ebp),
%eax
8(%ebp)是变址寻址方式,表示ebp+8地址的内容;而这条指令的含义就是将(ebp+8)中的内容赋值给eax寄存器。(ebp+8)所指向的内容就是栈标号2所代表的9;指令既是将9赋值给eax寄存器。此时eax值为9。[注:画的图仅为内存中栈的变化,而寄存器的变化则需要脑补]
第五句(13):movl %eax, (%esp)
将寄存器eax中的内容放到栈顶,其效果如下图:
第六句(14):call g
调用g(),call的工作是将当前“call
g”的下一条指令的eip存入栈中(根据代码显示应为15行的”leave”地址作为eip),并将标号g所在的地址视作下一条指令的地址,该指令等同于以下指令:
pushl %eip
movl 0x****, %eip
可进一步扩展为:
subl $4, %esp
movl %eip, (%esp)
movl 0x****, %eip
效果如下图:
标号g开始(1)
第一句(2):pushl %ebp
同上,栈顶增长4字节,然后再把ebp的内容存到栈顶,效果如下图:
第二句(3):movl %esp, %ebp
使得ebp指向esp指向的位置,效果如下图:
第三句(4):movl 8(%ebp), %eax
同上,8(%ebp)是变址寻址方式,表示ebp+8地址的内容;而这条指令的含义就是将(ebp+8)中的内容赋值给eax寄存器;(ebp+8)所指向的内容就是栈标号5所代表的9。指令即是将9赋值给eax寄存器。此时eax值为9。
第四句(5):addl $8, %eax
将一个立即数与eax寄存器中的数相加,最后结果存回eax寄存器中,此时eax寄存器值为17(9+8)。
第五句(6):popl %ebp
该条指令的作用是,将栈顶的内容放到ebp中去,然后栈顶指针减1(实际地址增加4个字节);
该指令相当于:
movl (%esp), (%ebp)
addl $4, %esp
此时汇编指令已经回到了上层函数,效果如下图:
第六句(7):ret
该语句相当于popl %eip
是将栈顶的元素置到eip寄存器中,此时计算机的当前指令为第15行指令,真正意义上的回到了上层函数,并把控制权交还给上层函数,效果如下图:
标号f(8)
第七句(15):leave
该指令相当于:
movl %ebp, %esp
popl %ebp
用该函数的栈基址指针直接覆盖栈顶指针,使得尽管函数中还有其他的“东西”在栈中,也可以直接跳过,退出函数。
效果如下图:
第八句(16):ret
该语句相当于popl %eip
是将栈顶的元素置到eip寄存器中,此时计算机的当前指令为第23行指令,真正意义上的回到了上层函数,并把控制权交还给上层函数,效果如下图:
标号main(17)
第六句(23):addl $6, %eax
将一个立即数与eax寄存器中的数相加,最后结果存回eax寄存器中,此时eax寄存器值为23(17+6)。
第七句(24):leave
该指令相当于:
movl %ebp, %esp
popl %ebp
用该函数的栈基址指针直接覆盖栈顶指针,使得尽管函数中还有其他的“东西”在栈中,也可以直接跳过,退出函数。
效果如下图:
第八句(25):ret
该语句相当于popl %eip
是将栈顶的元素置到eip寄存器中,退出了main函数。
至此,程序运行完毕。
3. 总结
读大学每个人都会有一个专业,而读计算机科学与技术的人需要用4年去认识、了解一台计算机,我们需要了解她的方方面面,需要了解她到底是如何工作的。
从宏观上讲,计算机的功能部件包括输入、输出、运算、存储、控制,而计算机的工作也是依托于它们的相互协作。这就是冯·诺依曼体系结构,而通过冯·诺依曼体系结构又将我们带到了“计算机是如何工作的”的微观世界;从微观上讲,存储程序的概念告诉我们存储在计算机中的程序的执行,是通过不断地重复“取指令、指令译码、执行指令”这三个过程实现的。
而一条条堆栈操作指令的确定,是通过ebp+esp的组合找到的;同样地,寄存器ecs+eip的组合也是确定当前代码段中当前指令的重要一环,当然了,它们都是寄存器,不在内存中。
这篇文章仅仅是对实验的流程进行解释说明,只是着重展示了计算机是如何工作的这一个话题,并不会面面具到地介绍冯·诺依曼体系结构、汇编语言语法、vim的使用等等。当然,仅仅聚焦在一点上会显得不足,若文章中有错误,欢迎大家指正。
陈金雷 + 原创作品转载请注明出处 +
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000