第三章 程序的机器级表示
教材知识总结
本章学习内容是汇编语言,一定要能读懂
-
X86 寻址方式经历三代:
- 1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
- 2 8086的分段模式
- 3 IA32的带保护模式的平坦模式
-
CPU包含一组8个存储32位值的寄存器
- 存整数数据和指针:eax,ecx,edx,ebx,esi,edi,esp,ebp。
- 大多数情况下前六个都用作通用寄存器,eax,ecx,edx的存储和恢复惯例不同于ebx,edi,esi(前三者为被调用者保存,后三者为调用者保存)。最后两个用于存储指针,由于在过处理中非常重要,分别指向栈帧的顶部和底部,必须保持。
-
gcc -S xxx.c -o xxx.s 获得汇编代码,也可以用objdump -d xxx 反汇编
-
注意: 64位机器上想要得到32代码:gcc -m32 -S xxx.c
Ubuntu中 gcc -S code.c (不带-O1) 产生的代码更接近教材中代码(删除"."开头的语句) -
二进制文件可以用od 命令查看,也可以用gdb的x命令查看。 有些输出内容过多
-
当一个源文件生成了'.o'的目标二进制文件后,无法直接查看。
-
但是还是有个查看目标代码文件内容的方法,就是对'.o'目标文件使用反汇编器。它的输出还是二进制文件,但是,反汇编器将这些二进制按照指令进行了分段。让我们知道哪一段是一个指令(格式上与汇编器产生的汇编文件一样,分行的)
表中不同数据的汇编代码后缀
-
数据传送指令有三个变种:movb(传送字节)movw(传送字)movl(传送双字)
-
寄存器:
-
esi edi可以用来操纵数组
-
esp ebp用来操纵栈帧
-
对于寄存器,特别是通用寄存器中的eax,ebx,ecx,edx,要理解32位的eax,16位的ax,8位的ah,al都是独立的
-
-
操作数的三种类型:立即数、寄存器、存储器
立即数:即常数值
寄存器:表示某个寄存器内容
存储器:根据计算出来的地址(通常称有效地址)访问某个存储器位置
有效地址的计算方式
- 符号扩展指令
-
pushl指令等价于:
subl $4,%esp movl %ebp,(%esp) //注意这里的括号引起的差别
-
popl指令等价于:
movl (%esp),%eax addl $4,%esp
-
栈顶元素的地址是所有栈中元素地址中最低的
-
一元操作和二元操作
-
1.一元操作
只有一个操作数,既是源又是目的,可以是一个寄存器,或者存储器位置。 例:加法运算符(++)和减1运算符
-
2.二元操作
-
第二个数既是源又是目的
第一个操作数可以是立即数、寄存器或者存储器位置
第二个操作数可以是寄存器或者存储器位置
两个操作数但是不能同时是存储器位置。
-
控制
-
一、条件码
CF:进位标志 ZF:零标志 SF:符号标志 OF:溢出标志
-
注意:
leal不改变条件码寄存器
CMP与SUB的区别:CMP也是根据两个操作数之差设置条件码,但只设置条件码而不更新目标寄存器
有条件跳转的条件看状态寄存器(教材上叫条件码寄存器)
-
控制中最核心的是跳转语句:
有条件跳转(实现if,switch,while,for)
无条件跳转jmp(实现goto)
当执行PC相关的寻址时,程序计数器的值是跳转指令后面那条指令的地址,而不是跳转指令本身的地址。
- (七) 过程
栈帧结构
为单个过程分配的栈叫做栈帧,寄存器%ebp为帧指针,而寄存器指针%esp为栈指针,程序执行时栈指针移动,大多数信息的访问都是相对于帧指针。
栈向低地址方向增长,而栈指针%esp指向栈顶元素。
-
转移控制
-
call:目标是指明被调用过程起始的指令地址,效果是将返回地址入栈,并跳转到被调用过程的起始处。
-
ret:从栈中弹出地址,并跳转到这个位置。
函数返回值存在%eax中
-
leave
这个指令可以使栈做好返回的准备,等价于: movl %ebp,%esp popl %ebp
-
-
查看函数调用栈信息的GDB命令
backtrace/bt n -
n是一个正整数,表示只打印栈顶上n层的栈信息。
-
n表一个负整数,表示只打印栈底下n层的栈信息。
frame n -
n是一个从0开始的整数,是栈中的层编号。这个指令的意思是移动到n指定的栈帧中去,并打印选中的栈的信息。如果没有n,则打印当前帧的信息。
up n -
表示向栈的上面移动n层,可以不打n,表示向上移动一层。
down n -
表示向栈的下面移动n层,可以不打n,表示向下移动一层。
教材中遇到的问题及解决以及感悟
-
3.1练习中有一道题是
260(%eax,%edx)
,我觉得这个好像题目并没有给出,后来对比下面发现这个260原来是十进制,然后转化为0x104就做出了答案 -
3.2练习中关于改变
push $0xff
的指令后缀,开始我考虑根据立即数$0xff
应该是pushw
,后来我发现对于栈操作都是双字操作,所以不管是pop
还是push
都应该使用pushl
和popl
-
3.4练习是关于类型转化的练习,有几个注意的地方
- 有符号扩展为无符号数使用符号扩展而不是零扩展
- 无论是有符号数还是无符号数在往更小的类型进行缩小的时候只需要使用低八位的寄存器。
-
3.7练习注意有几个二元操作的指令的用法与出现操作数的数量有关。
-
3.8练习移位量只能存储在单字节寄存器元素%cl中。
-
3.10练习是一个十分有趣的练习,他告诉了我们另外一种把寄存器的值置为0的操作,对比于更直接的
movl $0,%edx
,xorl的优势在于只需要两个字节,而movl需要五个字节。 -
P122页最下面有一段
movl 8(%ebp),%(edx)
、sarl $31,%edx
,这里做的是将16位的x进行符号扩展到32位,其中高16位放在%edx
中,低16位放在%eax
中。 -
3.12练习,刚开始做我有点懵,因为都做惯了双字的数据,没想到y竟然是四字的数据。要注意的是x*y_l乘积应该使用无符号乘法,而对y_h的低32位使用无符号和有符号都没有关系。
-
3.15练习我觉得很重要,做会这个基本就了解指令重定位的基本做法。
-
3.7节讲述的是函数在调用时,栈地址如何变化,如何使用特定的寄存器保护原来的数值。
代码情况
- 本周的学习基本都在书上,并没有进行代码的编写的。