程序的机器级表示
P104,p105:
X86,经历了一个长期的的不断发展的过程。开始时他是第一代单芯片和16位微处理器之一,由于当时集成电路技术水平十分有限,其中做了很多妥协。此后,他不断地成长,利用进步的技术满足更高性能和支持更高级的操作系统的需求。
8086(1978)
80286(1982)
i386(1985)
i486(1989)
Pentium(1993)
PentiumPro(1995)
Pentium II(1997)
Pentium III(1999)
Pentium 4(2000)
Pentium 4E(2004)
Core 2(2006)
Core i7(2008)
X86寻址方式经历三代:
1. DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
2. 8086的分段模式
3. IA32的带保护模式的平坦模式
摩尔定律:
随着Intel微处理器复杂性的复杂度提高,晶体管的数量不断增长。
P106:
ISA的定义:
计算级程序的格式和行为,定义为指令集体系结构,它定义了处理器状态指令的格式,以及每条指令对状态的影响。
一些对C语言程序员隐藏的处理器的状态是可见的:
程序计数器:
通常称为PC,指示将要执行的下一条指令在存储器的地址。
整数寄存器:
文件包含8个命名的位置,分别存储32位的值。
条件码寄存器:
保存最近执行的算术或者逻辑的状态和信息。它们用来实现控制或者数据流中的条件变化。
一组浮点寄存器:
用来存放浮点数据。
P107:
在命令行使用-s选项,就能得到C语言编译器产生的汇编代码:
unix>gcc -01 -s code.c
如果我们使用-c命令行选项,GCC会编译并汇编该代码:
unix>gcc -01 -c code.c
gcc -S xxx.c -o xxx.s 获得汇编代码,也可以用objdump -d xxx 反汇编。
注意:
64位机器上想要得到32代码:gcc -m32 -S xxx.c MAC OS中没有objdump, 有个基本等价的命令otool Ubuntu中 gcc -S code.c (不带-O1) 产生的代码更接近教材中代码(删除"."开头的语句)
P108:
如何找到程序的字节表示:
在文件code.o上运行GNU调试工具GDB输入命令:
(gdb) x/17xb sum
查看目标代码文件的内容输入命令:
unix>objdump -d code.o
生成可执行的文件prog:
unix>gcc -01 -o prog code.o main.c
反汇编文件prog:
unix>objdump -d prog
进制文件可以用od 命令查看,也可以用gdb的x命令查看。
有些输出内容过多,我们可以使用 more或less命令结合管道查看,也可以使用输出重定向来查看
od code.o | more
od code.o > code.txt
P109:
gcc -S 产生的汇编中以“.”开始的语句都删除
所有是我以“.”开头的行都是知道汇编器和链接器的命令。
我们通常可以忽略这些行。另一方面,没有关于这些指令的用途以及它们与源代码之间的关系的解释说明。
P110:
了解Linux和Windows的汇编格式有点区别:ATT格式和Intel格式
GCC采用的是AT&T的汇编格式, 也叫GAS格式(Gnu ASembler GNU汇编器), 而微软采用Intel的汇编格式.
一 基本语法
语法上主要有以下几个不同.
1、寄存器命名原则
AT&T | Intel | 说明 |
%eax | eax | Intel的不带百分号 |
2、源/目的操作数顺序
AT&T | Intel | 说明 |
movl %eax, %ebx | mov ebx, eax | Intel的目的操作数在前,源操作数在后 |
3、常数/立即数的格式
AT&T | Intel | 说明 |
movl $_value,%ebx | mov eax,_value | Intel的立即数前面不带$符号 |
movl $0xd00d,%ebx | mov ebx,0xd00d | 规则同样适用于16进制的立即数 |
4、操作数长度标识
AT&T | Intel | 说明 |
movw %ax,%bx | mov bx,ax | Intel的汇编中, 操作数的长度并不通过指令符号来标识 |
在AT&T的格式中, 每个操作都有一个字符后缀, 表明操作数的大小. 例如:mov指令有三种形式:
movb 传送字节
movw 传送字
movl 传送双字
因为在许多机器上, 32位数都称为长字(long word), 这是沿用以16位字为标准的时代的历史习惯造成的.
果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。比如指令“mov %ax, %bx”,由于目标操作数bx的长度为word,那么编译器将把此指令等同于“movw %ax, %bx”。同样道理,指令“mov $4, %ebx”等同于指令“movl $4, %ebx”,“push %al”等同于“pushb %al”。对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push $4”。
5、寻址方式
AT&T | Intel |
imm32(basepointer,indexpointer,indexscale) | [basepointer + indexpointer*indexscale + imm32) |
两种寻址的实际结果都应该是
imm32 + basepointer + indexpointer*indexscale
P111:
表中不同数据的汇编代码后缀
P112:
esi edi可以用来操纵数组,esp ebp用来操纵栈帧。
对于寄存器,特别是通用寄存器中的eax,ebx,ecx,edx,要理解32位的eax,16位的ax,8位的ah,al都是独立的,我们通过下面例子说明:
假定当前是32位x86机器,eax寄存器的值为0x8226,执行完addw $0x8266, %ax指令后eax的值是多少?
解析:0x8226+0x826=0x1044c, ax是16位寄存器,出现溢出,最高位的1会丢掉,剩下0x44c,不要以为eax是32位的不会发生溢出.
P113:
操作数指令符:
立即数:
在ATT格式的汇编代码中,立即数的书写方式$后面跟一个用标准C表示法表示的整数。任何能放进一个32位的字里的数值都可以用作立即数。
寄存器:
它表示某个寄存器的内容,对双操作数来说,可以是8个32位寄存器中的一个。
对于字操作数来说,可以是8个16位寄存器中的一个。
我们用Ea来表示任意的寄存器a,用引用R【Ea】来表示它的值
存储器:
它会根据计算出来的地址访问某个存储器的位置。
有效地址的计算方式 Imm(Eb,Ei,s) = Imm + R[Eb] + R[Ei]*s
P114:
MOV相当于C语言的赋值”=“
注意ATT格式中的方向
另外注意不能从内存地址直接MOV到另一个内存地址,要用寄存器中转一下。
MOV:
传送
MOVS:
传送符号扩展字节
MOVZ:
传送零扩展的字节
掌握push,pop:
p115/p116:
push :
ax是把ax里的值压入堆栈。即当前esp-4出的值变为ax的值,ax本身的值不变。
注意栈顶元素的地址是所有栈中元素地址中最低的。
p117:
指针就是地址;局部变量保存在寄存器中。
C语言中所谓的“指针”其实就是地址。
间接地引用指针就是将该指针放在一个寄存器中,然后在存储器中引用中使用这个寄存器。
其次,像X这样的局部变量通常是保存在寄存器中,而不是存储器中。寄存器访问比存储器访问要快的多。
p119:
结合表理解一下算术和逻辑运算
注意目的操作数都是什么类型:
加载有效地址:
指令leal实际上是movl指令的变形。它的指令形式是从存储器读数据到寄存器,但实际上它根本就没有引用存储器。
一元操作:
它只有一个操作数,既是源又是目的。
二元操作:
既是源又是目的。
移位:
先给出移位量,然后第二项给出的是要移位的位数。
特别注意:
1. 减法是谁减去谁
2.移位操作移位量可以是立即数或%cl中的数
p123:
条件码:
CF:进位标志
ZF:零标志
SF:符号标志
OF:溢出标志
CF:(unsigned)t < (unsiged)a 无符号溢出
ZF: (t==0) 零
SF: (t < 0) 负数
OF:(a <0 == b< 0)&& (t < 0 != a < 0) 有符号溢出
控制中最核心的是跳转语句:有条件跳转
p124:
有条件跳转的条件看状态寄存器(教材上叫条件码寄存器)
注意leal不改变条件码寄存器
思考一下:CMP和SUB用在什么地方
CMP指令根据它们的两个操作数之差来设置条件码
除了只设置条件码而不更新目标寄存器之外,CMP指令与SUB指令的行为是一样的。
p125:
条件码通常不会直接读取,常用的方法有三种:
1.可以根据条件码的某个组合,将一个字节设置为0或者1
2.可以条件跳转到程序的某个其他的部分
3.可以有条件的传送数据
setl和setb:
表示”小于时设置“和”低于时设置“
SET指令根据t=a-b的结果设置条件码
p127:
正常情况下执行下,指令按照它们出现的顺序一条一条的执行。
跳转指令会导致执行切换到程序中的一个全新的位置。
这些跳转的目的通常用一个标号指明。
p128:
(实现if,switch,while,for),
无条件跳转jmp(实现goto)
p130/p131:
if-else 的汇编结构:
t=test-expr;
if(!t)
goto false;
then-statement
goto done;
false:
else-statement
done:
汇编器为then-statement和else-statement产生各自的代码块。
它会插入条件和无条件分支,以保证执行正确的代码块。
p132/p133:
do-while:
loop:
body-statement
t=test-expr;
if(t)
goto loop;
也就是说,每次循环,程序会执行循环体里的语句,然后执行测试表达式。
如果测试为真,则回去再执行一个循环。
p134/p135:
while:
if(!test-expr)
goto done;
do
body-statement
while (test-expr);
done:
接下来,翻译成goto代码:
t=test-expr;
if(!t)
goto done;
loop:
body-statement
t=test-expr;
if(t)
goto loop;
done:
p137/p138:
for:
init-expr;
if(!test-expr)
goto done;
do{
body-statement
update-expr;
}while(test-expr);
done:
p144/p145:
switch:
p149:
IA32通过栈来实现过程调用。掌握栈帧结构,注意函数参数的压栈顺序.
p150/p151:
转移控制:
call 指令有一个指令目标,即指明呗调用过程起始的指令地址。同跳转一样,调用可以是直接的,也可以是间接的。
call指令的效果是将返回的地址入栈,并跳转到被调用过程的起始处
call/ret; 函数返回值存在%eax中
p174:
bt/frame/up/down :关于栈帧的gdb命令