X86(Intel处理器系列) 寻址方式经历三代:
1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
2 8086的分段模式
3 IA32的带保护模式的平坦模式
超线程:在一个处理器上同时运行两个程序
多核:将多个处理器实现在一个芯片上
对于机器级编程的两种重要抽象
1 机器级程序的格式和行为,定义为指令集体系结构(ISA)
定义了处理器状态,指令的格式,以及每条指令对状态的影响。将程序的行为描述成好像每条指令是按顺序执行的。
2机器级程序使用的存储器地址是虚拟地址,存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
程序计数器PC
指示将要执行的下一条指令在存储器中的地址。
gcc -S xxx.c -o xxx.s 获得汇编代码(这里不做进一步工作),gcc -c xxx.c -o xxx.o 产生目标文件,二进制格式。也可以用objdump -d xxx.o 反汇编; 注意函数前两条(push,mov )和后两条(pop,ret)汇编代码,所有函数都有,建立函数调用栈帧。
注意: 64位机器上想要得到32代码:gcc -m32 -S xxx.c
MAC OS中没有objdump, 有个基本等价的命令otool
Ubuntu中 gcc -S code.c (不带-O1) 产生的代码更接近教材中代码(删除"."开头的语句)
二进制文件可以用od 命令查看,也可以用gdb的x命令查看。
有些输出内容过多,我们可以使用 more或less命令结合管道查看,也可以使用输出重定向来查看
od code.o | more
od code.o > code.txt
gcc -S 产生的汇编代码中可以把 以”.“开始的语句都删除了再阅读(这些是指导汇编器和链接器的命令,可以忽略)
Linux和Windows的汇编格式的区别
1 Intel代码省略了指示大小的后缀
2 Intel代码省略了寄存器名字前面的‘%’符号
3 Intel代码用不同的方式来描述存储器中位置
4 在带有多个操作数的指令情况下,列出操作数的顺序相反
数据格式
一个 IA32中央处理单元CPU包含一组8个存储32位值的寄存器,用来存储整数数据和指针,名字都以%e开头。
esi edi可以用来操纵数组,esp ebp用来操纵栈帧。
多数情况下,前6个寄存器为通用寄存器,最后两个寄存器保存着指向程序栈中重要位置的指针。
另外,字节操作指令可以独立的读或者写前4个寄存器的2个低位字节。
对于寄存器,特别是通用寄存器中的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位的不会发生溢出.
操作数的三种类型
立即数:常数值
寄存器:某个寄存器的内容
存储器:根据计算出来的地址访问某个存储器位置
寻址模式
数据传送指令
将数据从一个位置复制到另一个位置的指令
MOV类中的指令将源操作数的值复制到目的操作数中,相当于C语言的赋值”=“。
注意ATT格式中的方向
注意不能从内存地址直接MOV到另一个内存地址(即两个操作数不能都指向存储器位置),要用寄存器中转一下。
MOVS和MOVZ指令类都是将一个较小的源数据复制到一个较大的数据位置,高位用符号位扩展(MOVS)或者零扩展(MOVZ)进行填充。
最后两个数据传送操作可以将数据压入程序栈中,以及从程序栈中弹出数据。
栈
1 遵循“后进先出”的原则
2 push压栈,pop出栈
3 栈顶:总是从这端插入和删除元素
4 栈顶元素的地址是最低的
5 栈指针%esp保存着栈顶元素的地址
指针
1 C语言中所谓的指针就是地址
2 间接引用指针是将该指针放在寄存器中然后在存储器引用中使用这个寄存器
3 局部变量通常保存在寄存器中。寄存器访问要比存储器快很多。
算术和逻辑操作
给出的每个指令类都有对字节、字、双字进行操作的指令,例如
addb
addw
addl
四组操作
加载有效地址:实际是将有效地址写入目的操作数,目的操作数必须是寄存器。
一元操作:只有一个操作数,可以是寄存器也可是存储器位置。
二元操作:源操作数是第一个,可以是立即数、寄存器、存储器
目的操作数是第二个,可以是寄存器、存储器
两个不能同时为存储器。
移位:第一个是移位量,用单个字节编码(只允许0-31位的移位),可以是立即数或者放在单字节寄存器%cl中
算术右移SAR,填上符号位;逻辑右移SHR,填上0。
目的操作数可以是一个寄存器或存储器。
控制中最核心的是跳转语句:
有条件跳转(实现if,switch,while,for)
无条件跳转jmp(实现goto)
条件码寄存器
描述了最近的算术或逻辑操作的属性,可以检测这些寄存器来执行条件分支指令
常用条件码:CF ZF SF OF
注意leal不改变任何条件码
访问条件码
1 根据条件码的某个组合,将一个字节设置为0或1。SET指令根据t=a-b的结果设置条件码
2 可以条件跳转到程序的某个其他部分
3 可以有条件的传送数据
跳转指令
会导致执行切换到程序中一个全新的位置,跳转的目的地通常用一个标号指明。
无条件跳转:JMP 可以是直接跳转也可以是间接跳转(写法是*后面加操作数指示符)
有条件跳转:根据条件码的某个组合,或者跳转或者继续执行下一条指令。
if-else汇编结构
通用形式会在两个分支语句中选择执行一个,汇编实现通过goto,就是汇编器为两个分支产生各自的代码块,它会插入条件和无条件分支,以保证能执行正确的代码块。
循环结构
do-while
while
for
汇编中用条件测试和跳转组合实现循环的效果。大多数汇编器根据do-while形式来产生循环代码,其他的循环会首先转换成do-while形式,然后再编译成机器代码。
switch
根据一个整数索引值进行多重分支。通过使用跳转表这种数据结构实现更加高效。跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当开关索引值为i时程序该做的。
这里的跳转可以用到goto/jmp
过程
过程调用包括将数据和控制从代码的一部分传递到另一部分,需要在进入时为过程的局部变量分配空间并在退出时释放空间,这通过程序栈实现。
IA32通过程序栈来实现过程调用。栈用来:
传递过程参数
存储返回信息
保存寄存器
本地存储
栈帧:为单个过程分配的那部分栈。
最顶端的栈帧以两个指针界定,寄存器%ebp为帧指针,寄存器%esp为栈指针。程序执行时,栈指针可以移动,大多数信息的访问都是相对于帧指针的。
栈向低地址方向增长。
call指令有一个目标,即指明被调用过程起始的指令地址,效果是将返回地址入栈,并跳转到被调用过程的起始处。
ret指令从栈中弹出地址,并跳转到这个位置,使用这个指令栈指针要指向call指令存储返回地址的位置。
函数返回值存在%eax中
bt/frame/up/down :关于栈帧的gdb命令
作业:
源代码:
gcc编译汇编语言:gcc –S –o main.s main.c -m32
从主文件夹中找到汇编文件:
整理汇编文件:删除gcc产生代码中以"."开头的编译器指令后得到的汇编代码:
add:
pushl %ebp ;将%ebp入栈,为帧指针
movl %esp, %ebp ;建立空帧栈
movl 8(%ebp), %eax ;空出地址存变量
addl $3, %eax ;ax中值加3
popl %ebp ;%ebp出栈
ret
call:
pushl %ebp ;将%ebp入栈,为帧指针
movl %esp, %ebp ;为call建立空帧栈
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call add ;调用add函数
leave ;为返回准备栈,相当于%ebp出栈
ret
main:
pushl %ebp ;将%ebp入栈,为帧指针
movl %esp, %ebp ;为main建立空帧栈
subl $4, %esp
movl $8, (%esp) ;
call call ;调用call函数
addl $1, %eax ;值加1
leave
ret