- 汇编语言有三部分构成:
- 汇编指令,由汇编器直接翻译成机器指令。
- 伪指令,给汇编器看的,告诉他如何翻译
- 符号体系,基本的运算,由汇编器负责解释。
- 内存的基本单元是Byte,CPU和内存通过地址总线,数据总线,控制总线来交流。
- 地址总线的根数决定了能使用的内存大小,寻址能力。
- 数据总线的根数决定了一次能够传输的位数。
- 控制线的根数决定了可能的操作数量。
- 8086具有20根地址线,具备1MB的寻址能力。16根数据线,一次能够传输2Byte的数据。
- 本身是16位的,即寄存器是16位的,能进行16位的运算。
- 地址空间中的地址不都是对应到内存单元,还有一些对应到显存单元,可以用来操作显示的画面。
- 内存,显存等块设备是通过地址来访问的,允许随机访问。
- 键盘,鼠标等字符设备是通过端口号来访问的,只能顺序访问。
- 8086 没有包含浮点指令部分(FPU),但是可以通过外接数学辅助处理器来增强浮点计算能力。Intel 8087 是标准版本。
-
AX,BX,CX,DX这四个是存放数据的通用寄存器。16位的,能表示0000H到FFFFH范围内的数据。都可以分割为两个8位寄存器,例如AX=AH+AL,分别为高8位和低8位。例如AX=33FFH,则AH=33H,AL=FFH。目的是为了兼容以前的8位程序,还有就是对应内存的最小单元8位。一个字=2个字节。
-
寄存器AX和AL通常称为累加器(Accumulator),用累加器进行的操作可能需要更少时间。累加器可用于乘、除、输入/输出等操作,它们的使用频率很高;
-
寄存器BX称为基地址寄存器(Base Register)。它可作为存储器指针来使用;
-
寄存器CX称为计数寄存器(Count Register)。在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数;
-
寄存器DX称为数据寄存器(Data Register)。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址。
-
mov ax,5 是将5H赋值给ax寄存器。
-
mov指令的两个操作数(数据和寄存器,寄存器和寄存器之间)需要位数相同,否则会报错:
mov ax,bl ;bl是8位,ax是16位,无法赋值。 mov bh,zx ;同上。 mov al 100 ;这里100默认是当做16进制的数,即100H,超过了8位,也会出错。 mov al 0005 ;会报错,因为0005H是16位的数据。 mov al 5 ;允许赋值。
-
add ax,5 是将ax寄存器中的数据和5H相加,然后赋值给ax寄存器。
-
当add操作的结果超过了要存入的寄存器:
mov ax,0 mov ax,93 ;此时ax=0093 add al,85 ;加法的结果为118,超过了8位,寄存器al会变为18。可见,不会自动进位到ah中。因为两个8位的寄存器是独立的。 mov ax,0 mov al,93 ;此时ax=93 add ax,85 ;进行的是16位加法,结果为118,可以完整保存到16位寄存器中。
-
不同位的寄存器之间不能相加减,例如add ax,bl会报错。
-
段地址寄存器有:ds,es,ss,cs。
-
偏移地址寄存器有:sp,bp,si,di,bx。
-
由于8086的地址线有20根,为了充分利用,于是引入了地址加法器,最终的地址为 段地址*16+偏移地址。
-
段地址*16的意思就是段地址向左偏移4位,变成了20位。
-
例如F230H:C8H 表示的地址为F230H*10H+C8H=F23C8
-
一个最终的物理地址可以有多种表示方式,例如11111H可以表示为1111H:1H或1110H:11H或1100H:111H等。即段和段之间是重复的。
-
CPU将CS:IP所指向的内容当做接下来要读取的指令。
-
一条指令的长短不一。CPU读取完后,先移动CS:IP,然后再执行指令。
-
jmp,跳转指令,用来修改CS和IP。8086不支持用mov指令修改CS和IP。
jmp 2000:0 ;修改cs为2000,ip为0 jmp ax ;cs不变,修改ip为ax寄存器的值。
-
call指令也是一条跳转指令,和jmp不同的是,他会记录下一行指令的IP的值,然后跳转到call的地址执行。如果后面执行到ret指令时,会将上一次记录的IP值赋值给当前的ip寄存器。因此CPU下一次取值的地方就变成了原来call的下一行指令,这样就完成了函数的调用和返回。
-
指令的执行过程:
- CPU从寄存器CS和IP对应的地址读取指令,存放到指令缓存器中。
- IP +=刚刚读取的指令的长度。
- 执行指令缓存器中的指令。
-
之所以不读取完指令后立即执行再自增IP,是因为要考虑到call指令的返回。因为call指令需要保存它的下一行指令的起始位置,这需要在IP自增后才可以。
-
而用户仿佛看不到指令的读取,只能看到指令的执行。
-
debug工具的用法:
r 查看或修改寄存器的内容 d 查看内存中的数据 u 将内存中的数据当做指令查看 a 用汇编指令写入内存 t 执行当前cs:ip的指令 e 修改内存中的数据
-
默认的多字节数据采用小端存储,即高位数据存放在高地址区域,例如 1234H这样的一个字型数据存放在内存2000H:0000H中为:
2000:0000 34 12
-
对于如下内存单元,从0地址处读取一个字节型数据为20H,字型数据为4E20H,从1地址处读取一个字节型数据为:4EH,字型数据为124EH。
2000:0000 20 4E 12 00
-
ds为数据段地址寄存器,一般用来访问内存:
mov al,ds:[0] ;将ds:0地址的一个字节型数据读取到al中。 mov ax,ds:[0] ;将ds:0地址的一个字型数据读取到ax中。 mov ax,[0] ;数据段默认使用ds。
-
可以根据寄存器的长度来确定,要读取和写入的内存单元的长度。
-
ds寄存器不支持mov直接修改,需要用寄存器赋值才可以:
mov ds,1000 ;会报错。 mov ax,1000 mov ds,ax ;这样才可以修改ds。
-
栈是一段连续的内存单元,只能操作栈顶的元素。
-
push和pop只能操作16位寄存器,push al是错误的。栈顶指针指向的是有意义的数据。
-
push ax
- 移动栈顶指针, sp = sp - 2
- 修改栈顶指针位置的数据。mov ss:sp,ax
-
pop ax,
- 将栈顶指针的数据赋值给ax。mov ax,ss:sp
- 然后移动栈顶指针。sp = sp + 2
-
8086中,SS:SP记录栈顶的位置。
-
第一次push 之前,ss:sp的位置就是栈底的位置,
-
SS寄存器不支持直接修改它的值,即mov ss,1000H会报错,需要通过寄存器mov。但是sp寄存器支持这样的操作。
-
mov ax,2000H mov ss,ax mov sp,10H ;这条指令会和上一条连续执行,不可打断。
-
CPU不会检查栈顶是否越界。SP寄存器的范围是0000H-FFFFH,最多可以存储64KB的数据,即32768个字型数据。
-
一直push可能会覆盖其他的数据内容,或者最早入栈的内容。
-
当寄存器不够用的时候,栈可以用来临时保存数据。栈也可以用来交换数据。
-
使用操作系统分配的内存空间是安全的。一种是操作系统在加载程序时,为程序分配,第二种是在程序的执行过程中,程序主动申请。
-
可执行文件中,除了要执行的指令,还有一些描述性信息,例如要分配的数据段的大小,程序的入口地址等。这些描述性信息在汇编源文件中用伪指令表示。
-
汇编器根据伪指令,会写入到EXE文件中如下的信息。
-
-
ds:0开始的256个字节是psp区,用户程序和系统通信的。后面才是用户定义的各种段。
-
如果数据的最开始是字母,则之前要加上0。例如:mov ax,B800H会报错,应修改为0B800H。
-
分号后面的内容是注释。
-
使用偏移地址寄存器来访问内存,可以通过自增来访问连续的数据:
mov bx,0 mov ax,ds:[bx] ;bx寄存器中保存的是偏移地址。默认的段地址寄存器为ds。 add bx,2 mov ax,ds:[bx] ;访问下一个字型数据 mov ax,ss:[bp] ;[bp]默认的段地址寄存器为ss。
-
寄存器的自增指令 inc ax,效果等同于add ax,1。不过前一条指令占字节少。
-
可以用jmp实现死循环,不过不允许在汇编代码中jmp到具体的地址,只能跳转到对应的标号:
mov dl,0 mark1: mov ds:[bx],dl inc bx inc dl jmp mark1 ;汇编器会将mark1标号替换为改行指令的地址。不过不允许手动写出该地址。
-
loop指令可以进行有条件的循环:
mov dl,0 mov cx,16 ;保存了循环要进行的次数,如果cx=1,则表示只运行一遍,不循环。类似于do-while循环。 mark2: mov ds:[bx],dl inc bx inc dl loop mark2 ;循环的范围是标号下面的三条指令。
-
指令执行的步骤:
- cx = cx -1
- 判断 cx 中的值,不为0则跳转(jmp)到标号的位置,继续执行,如果等于0,则执行loop下面的指令。
-
如果要在两个不同的数据段之间操作(例如memcpy),就要不断地修改ds,因此可以使用es段地址寄存器。es:[0]和ds:[0]都可以表示数据。
-
将数据,代码,栈放在不同的段中,内存由操作系统分配。
-
伪指令 dd(define dword),dw(define word),db(define byte),这里的dword是双字,即32个字节。
-
dup伪指令用来重复声明数据:
db 100 dup(0) ;定义了100个字节型数据,都是0。 dw 256 dup(1) ;定义了256个字型数据,都是1。 db 100 dup('abc def') ;定义了100段字节型数据,每段都是'abc def'。即700个字节。
-
一个分段的汇编语言程序:
assume cs:codeseg,ds:dataseg,ss:stackseg ;将这三个段寄存器和这三个段关联。 dataseg segment ;声明一个段,名称为data,其中分配了内存。段的名称可以随意取。 ;db 128 dup(0) dw 0123H,0456H,0789H,0ABCH,0DEFH,0FEDH,0CBAH,0987H ;define word,定义字型数据,一共8*2个字节。第一个字节的地址为ds:[0] dataseg ends stackseg segment stack ;声明一个段,名称为stack,后面的stack是声明该段为栈段。 ;db 128 dup(0) dw 0,0,0,0,0,0,0,0 ;第一个字节的地址为ss:[0] dw 0,0,0,0,0,0,0,0 ;定义了16个字型数据,一共占用32字节。 stackseg ends codeseg segment ;声明一个段,名称为code。 start: mov ax,dataseg ;第一个字节的地址为cs:[0] mov ds,ax ;数据段只需要设置段寄存区即可,没有段指针寄存器。 mov ax,stackseg mov ss,ax mov sp,32 ;需要设置栈段寄存器和栈顶指针寄存器。这里将栈的下限控制在ss:[0]的位置,当超过该位置,继续入栈时,不会覆盖掉前面的数据段。 mov bx,0B800H ;如果不显式声明代码段的开始,就必须要将该段紧挨着放在assume之后,否则会将dataseg的数据当做指令,开始执行。 mov es,bx mov bx,160*10+40*2 ;汇编器会自动计算160*10+40*2的值。这是符号体系, mov word ptr es:[bx],5535H mov ax,4c00H ;这两句是程序返回的意思,提示系统回收资源。 int 21H codeseg ends end start ;汇编指令的最终点。
-
分段以后,段和栈的引用都是在自己的段内,尽管这两个段在实际的内存中可能有重叠的部分。
-
每单独声明一个段,就会最少占用10H个字节,无论实际是否使用到这么多。这样做是为了保证不同的段都有各自独立的段地址。
-
si和di都可以当做偏移地址寄存器。
-
更灵活的定位内存地址的方法:
mov bx,0 mov si,0 mov ax,ds:[bx+si] mov ax,ds:[bx+si+16] mov ax,ds:[bx+5]
-
逻辑运算指令,and和or,进行的是位运算:
mov al,00001111B and al,11110000B ;计算al和11110000B按位与的结果,存储在al。 or al,11001100B ;计算al和11001100B按位或的结果,存储在al。
-
以字符形式给出数据:
db 48,49,50,51,52,53,54,55 ;定义字节型数据, db '01234567' ;等价于上面的一行。用ascii字符来定义。
-
ASCII码中英文字母大小写的编码是为了方便进行大小写转化:
字符 'A' 'a' 16进制编码 41H 61H 2进制编码 01000001B 01100001B
-
二者仅差1位,将大写字母 'A' |00100000B 就得到对应的小写字母'a' 。将小写字母'a' &11001111B就得到对应的大写字母'A'。
-
双重循环:
assume cs:code,ds:data data segment db 'ibm ' db 'dec ' db 'dos ' data ends stack segment stack ;定义一个32字节的栈。 dw 0,0,0,0 dw 0,0,0,0 dw 0,0,0,0 dw 0,0,0,0 stack ends code segment start: mov ax,data mov ds,ax mov bx,0 mov cx,4 mov ax,stack mov ss,ax mov sp,32 ;设置栈顶标记 Row: push cx ;将外层循环的cx保存在栈中,也可以保存在里边不会用到的寄存器dx中,还可以将cx保存到不会修改的数据段中。 mov cx,3 ;外层循环的次数 mov si,0 upLetter: mov al,ds:[bx+si] and al,11011111B mov ds:[bx+si],al inc si loop upLetter pop cx loop Row code ends end start
-
可以通过mov指令直接将数字写入到内存空间中,不过需要指明数字的字节数,即:
mov ds:[0],1 ;会报错,无法通过mov的两个操作数判断操作的内存单元是多大的。 mov byte ptr ds:[0],1 ;此时1被当做字节型数据。 mov word ptr ds:[0],1 ;此时1被当做字型数据。 add word ptr ds:[0],5588H inc word ptr ds:[0] mov word ptr ds:[bx+si+16],'a'
-
div除法指令,有8位和16位两种。被除数/除数=商+余数
被除数:默认放在AX或者AX,DX中 如果除数为8位,被除数则为16位,默认存放在AX中 如果除数为16位,被除数则为32位,DX存放高16位,AX存放低16位 结果: 如果除数为8位,则AL存储商,AH存储余数 如果除数位16位,则AX存储商,DX存储余数
-
除数可以在内存中(使用byte ptr或word ptr),也可以在寄存器中。
-
mov ax,16 mov bl,3 div bl ;8位除法。商为05H存放在al中,余数为01H存放在ah中 ,此时ax=0105H。 mov ax,FFDDH mov dx,1122H mov bx,2233H div bx ;16位除法1122FFDDH/2233H。商为8047H,存放在ax中;余数为03b8H,存放在dx中。
-
能够修改IP或cs的指令,有jmp,loop,jcxz。
-
offset伪指令,可以获得标号的偏移地址:
s: mov ax,4 mov ax, OFFSET s ;将s标号的偏移地址赋值给ax。
-
jmp指令的机器码:
jmp s ;jmp指令的机器码中并非是s指令所在的内存位置,而是从s标号相对于jmp指令后第一个字节的偏移,即偏移地址的差值。 mov bx,1000 s: mov ax,1222
-
jmp指令中存储的是相对位移,这样该段程序移动到内存的任意位置,都可以成功运行。方便函数的使用。
-
jmp指令的长度可以为2或3个字节,其中后1或2个字节表示跳转的相对位置,分为8位位移(-128~127)和16位位移(-32768~32767)。负数表示向上跳转。
-
CPU遇到nop,什么也不做。该指令占用1个字节。
-
jcxz 条件转移指令 当cx寄存器=0时,进行jmp,和loop正好相反。所有的条件转移指令都是短转移,位移范围是-128~127。该指令不会修改cx寄存器的值。
-
配合loop实现循环的条件退出:
mov bx,0 s: mov ch,0 mov cl,ds:[bx] ;这段程序从内存中依次读入字节型数据,放入cl中。 jcxz ok ;判断cx == 0,如果相等就跳转到ok标号处,否则执行下一句。 inc bx ;需要先判断,再自增。 loop s ;上面由于cx!=0,没有跳转,因此到了里,loop自然成立,跳转到标号s处执行。 ok: mov dx,bx ;如果找到了=0的字节型数据,则将偏移放到dx中。
-
如果在loop指令内修改cx的值,可以死循环。
-
jmp far ptr s1 ;这条指令中会存放s1标号的真实地址,而非偏移地址。变成 jmp 1122:3344H的样子。占用5个字节。 mov ax,2233H jmp ax ;会将ip寄存器的值设置为ax的值。跳转到该位置执行。 jmp word ptr ds:[0] ;从ds:[0]中读取一个字型数据,给ip,跳转到那里执行。 jmp dword ptr ds:[0] ;从ds:[0]中读取一个双字型数据,给cs:ip,跳转到那里执行。相当于mov ip,ds:[0] mov cs,ds[0+2]
-
可以将函数的入口地址存储到内存中(函数指针),然后用jmp word ptr ds:[0]之类的语句来跳转过去执行。
-
80x25彩色字符显示缓冲区(B8000H~BFFFFH),字符和颜色间隔放置,各占一个字节。一共4KB,显示缓冲区分为8页,一共32KB,可以显示任意一页的内容,一般情况下只显示第一页。
-
字符的颜色占用1个字节,012和456位分别表示前景和背景色(rgb),第3位和第7位分别表示高亮和闪烁。这里的前景色指的是字体颜色。
-
ret(return)相当于 pop ip ,即将栈顶的元素赋值给ip。retf(return far)相当于pop ip和pop cs 。
-
call s ;执行的时候会先将push ip,再jmp near ptr s。这里的ip是读取完call之后的ip。即call下一条指令的地址。call指令中存在的是16位的相对位移。 call far ptr s ;执行的时候会先push cs和push ip,再jmp far ptr s类似。这种指令中存储的是s的真实地址,而非偏移。如果要返回需要使用retf。 call ax ;相当于ip=ax call word ptr ds:[0] ;push ip,跳转到对应的地方。 call dword ptr ds:[0] ;push ip和cs,跳转到对应的地方。
-
mul乘法指令:分为8位乘法和16位乘法,两个乘数的位数要相同。
mul bl ;8位乘法,一个乘数默认存放在al中,结果存放在ax寄存器中。 mul byte ptr ds:[0] mul bx ;16位乘法,一个乘数默认存放在ax中,结果为32位的,低16位存放在ax中,高16位存放在dx中。 mul word ptr ds:[0]
-
标志位寄存器是1个寄存器,共有16位,其中的8位用来表示标志。他们又称为程序状态字,Program Status Word
-
-
-
标志位寄存器只会在算术指令结束后进行设置,例如add,sub,mul,neg,cmp等。而mov数据传输指令等不会设置。
-
CF(carry flag)是进位标志位,表示在最高位发生了进位。只有计算指令能够改变它,mov指令不会。
-
ZF(zero flag)是零位标志位,该标志表示运算的结果是否为0。
-
PF(parity flag)是奇偶标志位,表示运算的结果中1的个数是奇数还是偶数,偶数时为真。
-
AF( Auxiliary Carry flag)是扶助进位标志,用于BCD码计算中的进位记录。
-
SF(sign flag)是符号标志位,表示运算的结果是正数还是负数。按照最高位来区分。
-
OF(overflow flag)是溢出标志位,表示计算的结果和要存放的目标相比,是否能足够存放的下。正数+负数永远不会发生溢出。两个正数或两个负数相加容易发生溢出,导致结果变号,即最高位改变。
-
进位和溢出的区别:
-
由于CPU不知道程序员是把寄存器中的值当做有符号还是无符号数,因此它都要考虑到,就把寄存器中的值当做位串进行运算,只知道按位地操作。
-
如果某位上发生了1+1,则会进位,将该位设为0。因此进位是朴素的,一般用来描述有符号数。而把特定的数当做有符号数来看待时,就还可以考虑是否发生溢出。
-
对于无符号数来说,如果发生了进位,那么结果就不准确了。
-
对于有符号数来说,如果发生了溢出,结果也不准确了。
-
溢出的例子:
-
mov al,65H ;al寄存器中存放的是 0110 0101B这8个位的数据。有符号数为101,无符号数为101。 mov bl,73H ;bl寄存器中存放的是 0111 0101B这8个位的数据。有符号数为115,无符号数为115。 add al,bl ;计算的结果为 1101 1010B 可以看到此次计算并没有发生进位。有符号数的结果为216(超出了8位有符号数的范围,8位结果为-38),无符号数的结果为216。 ;通过最高位判断,这次加法是两个正数相加,结果是负数,说明发生溢出了。
-
进位的例子:
mov al,E5H ;al寄存器中存放的是 1110 0101B这8个位的数据。有符号数为-27,无符号数为229。 mov bl,45H ;bl寄存器中存放的是 0100 0101B这8个位的数据。有符号数为69,无符号数为69。 add al,bl ;计算的结果为 1 0010 1010B 可以看到此次计算发生了进位。有符号数的结果为42,无符号数的结果为298(超过了8位无符号数的范围,8位结果为42)。
-
既进位又溢出的例子:
mov al,80H ;al寄存器中存放的是 1000 0000B这8个位的数据。有符号数为-128,无符号数为128。 mov bl,80H ;bl寄存器中存放的是 1000 0000B这8个位的数据。有符号数为-128,无符号数为69。 add al,bl ;计算的结果为 1 0000 0000B 可以看到发生了进位,有符号数和无符号数结果均为0。两个负数相加最高位变成了0,发生了溢出。
-
溢出和进位标志位都是判断中间结果是否发生了进位和溢出,而符号标志位是表示最终结果是正还是负。
-
adc(add carry)带进位的加法指令,运算时,会加上进位标志的结果:
adc ax,bx ;ax的结果为ax+bx+carry。 carry表示上一次运算是否进位。 ;例如用一个只会计算一位数加法的程序来计算 18+25 ,先计算8+5会产生进位,此时将carry置为1,结果为3。然后计算 1+2+carry结果为4,没有进位。因此最终的结果为43。
-
sbb是带进位的减法。
sbb ax,bx ;ax的结果为ax-bx-carry。例如用一个只会计算一位数减法的程序来计算 43-18,先计算3-8,需要借位,此时将carry置为1,结果为5。然后计算4-1-carry,结果为2,没有进位。因此最终结果为25。
-
通过带进位的加减法,可以将只能计算16位或32位加减法的指令通过循环变成,可以计算64甚至128位加减法的指令。
-
cmp比较指令,相当于做减法,不过只影响标志位,不改变寄存器。
cmp ax,bx ;计算ax-bx的值,据此设置标志位。ax和bx的值都不会变。
-
cmp指令一般配合条件跳转来使用:
cmp ax,bx je s ;jump equal 当零标志位为真时跳转。即ax = bx 跳转的范围在-128~127。 jne s ;jump not equal 当零标志为假时跳转。即 ax != bx jb s ;jump below 当 。即a < b jnb s ;jump not below 当 。即a >= b ja s ;jump above 当 。即a > b jna s ;jump not above 当 。即a <= b
-
cmp的减法可以看做是有符号也可以是无符号的,因此跳转也分两大类。
-
-
如果要设定在 2 < ax < 5时再跳转到s1否则不跳转执行
if (ax <5 && ax >2){ s1 }else{ s2 } s3
,可以用如下的代码:
cmp ax,2 jna s2 cmp ax,5 jnb s2 s1: mov ax,0 jmp s3 s2: mov bx,0 jmp s3 s3: mov cx,0
-
DF方向位置标志位。
cld ;设置DF标志位为UP,即地址增大的方向。 rep movsb ;重复执行cx次的movsb操作,即复制字节型数据,从ds:[si]到es:[di]。每复制一个字节,就inc si和inc di。movsw是复制字型数据。
-
rep movsb称为串传送指令。将一段连续的数据移动到另一段连续的空间。
-
pushf和popf是将标志位寄存器入栈和出栈,用于临时保存标志位寄存器。也可以通过这个来间接修改标志位寄存器的值。
mov ax,0 push ax popf ;设置标志位寄存器为全0
-
中断就是发生了需要CPU立刻去处理事情。
-
常见的中断:
- 除法错误
- 单步调试执行
- 执行into指令
- 执行int指令
-
CPU通过调用相应的中断处理程序来处理。中断处理程序的地址(cs:ip)集中存放在中断向量表中,在0000:0000~0000:03FF的地方,可以登记256个中断处理程序。
-
中断向量表就是一个函数指针数组。数组的下标是中断类型码。 3号中断类型码的IP为0000:[3*4],CS为0000:[3*4+2]。
-
int 0 ;执行这条指令会将修改cs和ip的值为中断向量表中0号中段的入口地址。然后跳转到改地址执行。0号中断对应的是除法错误,因此会打印出 divide overflow,然后强制退出程序。
-
中断的处理过程:
- 取得中断类型码
- 保存标志位寄存器,即pushf
- 将标志位寄存器的第8 TF 和9位 IF 设置为0。 这样做的目的是为了防止中断处理程序中被另一个中断打断。
- 保存cs和ip
- 根据中断类型码,找到对应的中断处理函数的地址,赋值给cs和ip。
- 执行中断处理程序
-
中断处理程序的最后会执行iret来恢复现场(栈和标志位寄存器),然后返回继续执行下一条指令。
-
可以修改中断向量表中的函数地址,来让CPU使用自己编写的中断处理函数。
-
中断可以由CPU运算的过程中自动产生,也可以由程序主动调用(使用int命令)。
-
BIOS和DOS系统提供了很多中断处理程序用来丰富系统的功能。
-
BIOS的功能:
- 硬件检测和初始化。
- 将外部中断和内部中断处理程序登记在内存中的中断向量表中。
- 将用于对硬件设备进行I/O操作的中断处理程序登记在内存中的中断向量表中。
- 将其他相关硬件的中断处理程序登记在中断向量表。
-
开机后的动作:
- CPU先将cs和ip置为ffff:0000,该处的指令为jmp到BIOS程序的起始位置。
- BIOS将中断处理程序登记到中断向量表中。
- 调用int 19H,进行系统的引导。
- 操作系统启动时,也将自己的中断处理程序登记到中断向量表中。
-
通过int指令,触发BIOS和系统提供的中断处理程序时,应该通过一些寄存器来设置该中断处理程序的参数。
-
当按下键盘上的一个键时,键盘通过自己的芯片将该键的编号发送到和CPU相连接的端口中,CPU使用in和out来读写该端口的数据。不同于用于读写内存单元的mov指令。
-
端口号用2个字节表示,范围从0-65535。只能用al或ax来存放要读写的数据。
-
in al,键盘端口1 ;从端口中读取一个字节的数据,放到al中。 mov al,10 out 70H,al ;由于CMOS端口存储着128个字节,因此需要先指定要从哪个字节(0-127)开始读取。 in al 71H ;然后使用in 指令将指定端口的数据读取到al寄存器中。 mov al,5 out 71H,al ;通过al将数据写入CMOS的第10个字节。
-
CMOS RAM中有128个字节,其中0-D保存的是时间信息,其余大部分是硬件信息(用于给BIOS初始化)。有电池维持运转。
-
CMOS有两个端口,70H(地址端口,决定要读写的位置)和71H(数据端口,数据通过该端口进出)。
-
逻辑移动指令:shl和shr。移位运算相当于乘以或除以2
mov al,00000001B shl al,1 ;将al寄存器向左移动1位,右侧补零,结果保存在al中,此时al为00000010B mov bl,10001000B shr bl,1 ;将bl寄存器向右移动1位,左侧补零,结果保存在bl中,此时bl为01000100B mov cl,4 mov al,1 shl al,cl ;一次移动多位
-
会将移出的那一位,放到CF标志位中。
-
CMOS中的时间信息使用BCD码存储,即将十进制数拆分为一个一个的位,每位用4二进制位表示。例如45可以表示为 0100 0101。 因此使用一个字节可以表示两位十进制数字。时间信息会一直刷新。
-
;秒 分 小时 日 月 年 ;0 2 4 7 8 9 ;都是使用一个字节来存储对应的信息,年的话只保存后两位,即2005年会保存为05。 mov al,8 ;读取月份字节。 out 70H,al in 71H,al ;当前为12月,即al=0001 0010B mov ah,al mov cl,4 shr ah,cl ;调整ah为al中的高4位 and al,00001111B ;调整al为原来的第4位 add al,30H ;将数字转化为对应的ASCII码,方便输出。类似于itoa() add ah,30H
-
中断的本质是硬件轮询:CPU在每个指令周期,在执行完指令之后,会去查看中断寄存器的状态,如果有中断,那么就跳转到中断处理程序。
-
-
中断可以分为硬件中断和软件中断:
- 硬件中断(Hardware Interrupt)
- 可屏蔽中断(maskable interrupt)。硬件中断的一类,可通过在标志位寄存器中设置来觉得是否屏蔽。
- 非可屏蔽中断(non-maskable interrupt,NMI)。硬件中断的一类,无法通过设置标志位寄存器来关闭。典型例子是时钟中断(一个硬件时钟以恒定频率—如50Hz—发出的中断)。
- 处理器间中断(interprocessor interrupt)。一种特殊的硬件中断。由处理器发出,被其它处理器接收。仅见于多处理器系统,以便于处理器间通信或同步。
- 伪中断(spurious interrupt)。一类不希望被产生的硬件中断。发生的原因有很多种,如中断线路上电气信号异常,或是中断请求设备本身有问题。
- 软件中断(Software Interrupt)
- 软件中断。是一条CPU指令,用以自陷一个中断。由于软中断指令通常要运行一个切换CPU至内核态(Kernel Mode/Ring 0)的子例程,它常被用作实现系统调用(System call)。
- 硬件中断(Hardware Interrupt)
-
外中断中的外指的是外设备,即键盘,鼠标等设备。中断程序也是放在中断向量表中。
-
所有的中断可以分为两种:
- 可屏蔽:例如键盘中断,可以设置标志位寄存器来屏蔽该中断
- 不可屏蔽:CPU必须相应
-
cli(clear interrupt)和sti(set interrupt)指令,分别是将IF设置为0和1。执行cli后,如果再出发中断,CPU会根据中断类型码检查该中断是否是可屏蔽,如果是,则不忽略。该指令对不可屏蔽中断无用。
-
有些事情(例如,修改中断向量表)不希望被键盘中断打断,此时可以执行cli来屏蔽。
-
键盘使用扫描码来和计算机通信,表示那些键被按下了。键盘的读写端口为60H。中断类型码为9,可屏蔽。一个按键按下扫描码为通码,抬起时,扫描码为断码,断码=通码+80H。
-
键盘的中断处理程序是由BIOS提供的,处理步骤如下:
- 读取60端口的扫描码。
- 如果该扫描码是字符键(ASCII可显示字符),则将扫描码和对应的ASCII吗放到BIOS中的键盘缓冲区,在内存中。15个字型数据(15对,高位存放扫描码,低位存放ASCII码)
- 如果该扫描码是控制键,则将其转变为状态字节,记录到一个内存单元中 0040:0017。
- 对键盘系统的相关控制,例如对相关芯片发出应答信息。
-
按下键盘,相当于执行了一个int 9,CPU根据标志寄存器,来决定是否要响应该中断。
-
可以自己写int 9的中断响应程序,实际上是对BIOS中断处理程序的封装,可以增加一些功能:
- 修改中断向量表中对应的cs和ip,指向自己写的中断响应程序。
- 在自己的程序中,调用BIOS提供的中断处理程序(使用call 调用,需要先设置al=60H,读取键盘端口),最后要用iret返回。
-
linux内核就是封装覆盖了BIOS提供的中断。
-
BIOS的int 9中断例程和int 16h中断例程是一对相互配合的程序,int 9中断例程向键盘缓冲区中写入,int 16h中断例程从缓冲区中读出(ah为扫描码,al为ASCII吗)。它们写入和读出的时机不同,int 9中断例程是在有按键按下的时候向键盘缓冲区中写入数据;而int 16h中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。
-
如果填满了缓冲区会报警,蜂鸣声,覆盖掉最开始的。
-
int 16的流程:
- 检测键盘缓冲区是否有数据,没有的话会继续检测。
- 读取缓冲区的第一个字型数据,高位字节给ah,低位字节给al。
- 删除缓冲区的第一个字型数据
-
int 13H 是用来对磁盘进行读写的。这里读取的是扇区(物理层面的),不依赖于文件系统。扇区号是从1开始的。
mov ax,0 mov es,ax mov bx,200H ;设置读写的内容对应的内存地址。 mov al,1 ;读取的扇区个数 mov ch,0 ;磁道 mov cl,1 ;扇区 mov dl,0 ;驱动器号,如果有多个驱动器的话。 mov dh,0 ;面号 mov ah,2 ;功能号2表示读取,3表示写入。 int 13H
-
DOS系统工作中CPU工作在实模式下。现在的操作系统,CPU都是工作在保护模式下,许多中断都不能直接使用。
-
汇编操作寄存器,C语言操作变量更灵活。C的函数调用传参和返回值都是自动完成的。也不会手动分配不同的段。
-
_asm { //C语言内嵌汇编 mov eax,3 }
-
不同编译器将C编译出的汇编不同,C标准也没有规定这些。
-
内联汇编的部分,编译器不进行修改。
-
主调函数
-
6: void main() 7: { 00C316F0 55 push ebp 00C316F1 8B EC mov ebp,esp 00C316F3 81 EC E4 00 00 00 sub esp,0E4h 00C316F9 53 push ebx 00C316FA 56 push esi 00C316FB 57 push edi 00C316FC 8D BD 1C FF FF FF lea edi,[ebp-0E4h] 00C31702 B9 39 00 00 00 mov ecx,39h 00C31707 B8 CC CC CC CC mov eax,0CCCCCCCCh 00C3170C F3 AB rep stos dword ptr es:[edi] 00C3170E B9 00 A0 C3 00 mov ecx,offset _1A512F26_test@c (0C3A000h) 00C31713 E8 EB FA FF FF call @__CheckForDebuggerJustMyCode@4 (0C31203h) 8: 9: int a = 3, b = 4, c = 5; 00C31718 C7 45 F8 03 00 00 00 mov dword ptr [a],3 ;组织变量,连续存储 00C3171F C7 45 EC 04 00 00 00 mov dword ptr [b],4 00C31726 C7 45 E0 05 00 00 00 mov dword ptr [c],5 10: a = plus(b,c); 00C3172D 8B 45 E0 mov eax,dword ptr [c] 00C31730 50 push eax ;参数入栈,由于参数是存在变量中,所以要先读入到寄存器中 00C31731 8B 4D EC mov ecx,dword ptr [b] 00C31734 51 push ecx ;入栈的顺序是从右向左 00C31735 E8 36 FC FF FF call _plus (0C31370h) ;会将下一条指令地址(00C3173A)入栈。然后执行这一行,00C31370 E9 2B 0A 00 00 jmp plus (00C3DA0h) ;jmp指令不改变栈结构。并没有直接跳转到plus的代码执行,而是中转了一下。 00C3173A 83 C4 08 add esp,8 ;将函数调用时入栈的2个参数,移出栈顶。外堆栈平衡。 00C3173D 89 45 F8 mov dword ptr [a],eax ;默认的返回值存放在eax中。 11: 12: } 00C31740 33 C0 xor eax,eax ;将eax置为0,表示返回值为0。效率比mov高,指令也短。 00C31742 5F pop edi 00C31743 5E pop esi 00C31744 5B pop ebx 00C31745 81 C4 E4 00 00 00 add esp,0E4h 00C3174B 3B EC cmp ebp,esp 00C3174D E8 BB FA FF FF call __RTC_CheckEsp (0C3120Dh) 00C31752 8B E5 mov esp,ebp 00C31754 5D pop ebp 00C31755 C3 ret
-
被调函数
-
1: int plus(int x, int y) { 00C31DA0 55 push ebp ;保存原来的栈底位置, 00C31DA1 8B EC mov ebp,esp ;提升当前栈底指针为当前栈顶指针。即栈收缩为0 00C31DA3 81 EC C0 00 00 00 sub esp,0C0h ;创建缓冲区。192个字节,48个栈单元。 00C31DA9 53 push ebx ;保存现场,依次push寄存器,最后逆序pop。 00C31DAA 56 push esi 00C31DAB 57 push edi 00C31DAC 8D BD 40 FF FF FF lea edi,[ebp-0C0h] ;edi指向缓冲区的低地址头 00C31DB2 B9 30 00 00 00 mov ecx,30h ;写入30H次,每次4个字节。一共是192个字节,即整个缓冲区的大小。 00C31DB7 B8 CC CC CC CC mov eax,0CCCCCCCCh ;向缓冲区填充0xcc字节数据。这样做是为了防止程序出错执行这一部分,之所以用cc覆盖,是因为cc就是断点对应的机器码。 00C31DBC F3 AB rep stos dword ptr es:[edi];重复stos指令30H次,stos是将eax的内容存放到es:[edi]开头的4个字节中。edi会顺着方向变化。 00C31DBE B9 00 A0 C3 00 mov ecx,offset _1A512F26_test@c (0C3A000h) 00C31DC3 E8 3B F4 FF FF call @__CheckForDebuggerJustMyCode@4 (0C31203h) 2: 3: return x + y; 00C31DC8 8B 45 08 mov eax,dword ptr [x] ;有的地方使用[ebp+8]来获取函数的参数。 00C31DCB 03 45 0C add eax,dword ptr [y] ;局部变量的地址为[ebp+0C],偏移得来的。 4: } 00C31DCE 5F pop edi ;逆序恢复寄存器的值。 4: } 00C31DCF 5E pop esi 00C31DD0 5B pop ebx 00C31DD1 81 C4 C0 00 00 00 add esp,0C0h 00C31DD7 3B EC cmp ebp,esp 00C31DD9 E8 2F F4 FF FF call __RTC_CheckEsp (0C3120Dh) ;该函数用来检查对战是否平衡,只在debug中存在,release中就没了。 00C31DDE 8B E5 mov esp,ebp ;回收缓冲区,栈中的内存,使用完后回收,只是移动栈顶和栈底指针,并不会清零。只有下一次使用才会清零。 00C31DE0 5D pop ebp ;恢复原来的栈底,栈顶指针也移动 00C31DE1 C3 ret ;从栈顶pop 出返回地址,此时栈顶恢复到刚入栈完参数的样子。
-
ESP是栈顶指针,EBP是栈底指针。栈底指针指向的内容不是栈中的元素。
-
gbk编码cccc表示字符“烫”。由于缓冲区的保护字符为0xcc,因此在gbk编码的程序中,出错时会出现烫烫烫。
-
ebp指向0x00bafb18,ebp内存储的是上一个函数的栈底(是有当前函数第一个指令 push ebp 压入栈的),ebp+4为程序的返回地址,ebp+8为第一个参数,ebp+12为第二个参数等等。
-
-
函数执行完毕,堆栈平衡,eax发生了变化,存储着返回值,栈的增长方向多了一些垃圾(参数,缓冲区等)。
-
如果函数没有return内容,那么写不写return都是一样的。
- 全局变量的初始化使用的是全局的内存地址(写在了机器码中),因此任何程序都可以修改(外挂也可以修改)。而局部变量的初始化使用的是偏移地址。
-
- 全局变量在编译时就确定了内存地址和宽度,如果不重新编译,全局变量的地址是不会变的。游戏外挂中的找基址就是找全局变量。
- 局部变量只有当函数执行时,才会在栈中分配空间,它的地址也是不确定的(因此只能在函数内使用),因为不知道函数何时会被调用,无法提前分配。只能根据堆栈的指针偏移来索引。
- 全局变量声明的时候就会分配内存空间,而局部变量只有在初始化的时候才会分配空间。
-
- 局部变量的分配:机器指令的最后一个字节是指该变量的地址相对于栈底的偏移量。前两个为函数的参数,[ebp+8],[ebp+12],F8实际上是-8,即局部变量z是分配在 [ebp-8]的地方,即缓冲区。
-
- 可见,函数的参数是存放到调用它的函数的栈顶,也就是当前ebp的下面,而函数内部新的局部变量是分配在缓冲区内的。
- 缓冲区就是存储函数的局部变量的,参数不在这里。因此局部变量在初始化之前,都是填充的0xcc。
- 缓冲区溢出,就是通过修改缓冲区,达到修改ebp+4的内容(即当前函数的返回地址),进而达到控制程序的目的。
-
整数在进行扩展时,要注意的问题:
signed char c = -1; //内存中存储的为0xFF。这是编译器做的工作。 int x = c; //8位→32位,低位直接复制,因为c为有符号数,因此扩展的时候高位补符号位。所以x为0xFFFFFFFF unsigned char c = -1; //内存中存储的为0xFF。也就是255。 int x = c; //无符号数扩展,高位一律补0。
-
扩展使用的指令:
-
-
转义字符的\n中的\是给编译器看的。
-
字符串之所以要以'\0'作为结尾,主要是需要在内存中标识出来。
-
栈空间中的字符串:
11: char str[] = "Hello World\n"; ;13个字符,包括尾零。 00383C48 A1 CC 6B 38 00 mov eax,dword ptr [string "Hello World\n" (0386BCCh)];堆上的字符串。 00383C4D 89 45 EC mov dword ptr [str],eax 00383C50 8B 0D D0 6B 38 00 mov ecx,dword ptr ds:[386BD0h] 00383C56 89 4D F0 mov dword ptr [ebp-10h],ecx 00383C59 8B 15 D4 6B 38 00 mov edx,dword ptr ds:[386BD4h] 00383C5F 89 55 F4 mov dword ptr [ebp-0Ch],edx 00383C62 A0 D8 6B 38 00 mov al,byte ptr ds:[00386BD8h] 00383C67 88 45 F8 mov byte ptr [ebp-8],al
-
ASCII后来又增加了128个,称为扩展ASCII,中文编码GB2312是用两个字节来表示一个汉字,每个字节的范围是0xA0-0xFE(94区,94位,不过有些区是空的),大约能表示7000多个汉字。gbk编码也是用2个字节表示一个汉字,不过第二个字节可以和ASCII码重复。
-
GB2312只能表示部分汉字,GBK可以表示中日韩的大部分文字。二者都是兼容ASCII码的,即都用一个字节表示。
-
不同类型计算中,隐式类型转换,short和char都会通过movsx指令转为int类型。如果计算的结果要赋值给short类型,则会用寄存器的低16位赋值。
-
12: short x = 3; 0065419A B8 03 00 00 00 mov eax,3 0065419F 66 89 45 E0 mov word ptr [x],ax ;一个字 13: char y = 5; 006541A3 C6 45 D7 05 mov byte ptr [y],5 ;一个字节 14: x = x + y; 006541A7 0F BF 45 E0 movsx eax,word ptr [x] 006541AB 0F BE 4D D7 movsx ecx,byte ptr [y] 006541AF 03 C1 add eax,ecx 006541B1 66 89 45 E0 mov word ptr [x],ax ;eax的低16位。
-
自增运算符:
12: int x = 3, y = 4; 0056419A C7 45 E0 03 00 00 00 mov dword ptr [x],3 005641A1 C7 45 D4 04 00 00 00 mov dword ptr [y],4 13: y = x++; 005641A8 8B 45 E0 mov eax,dword ptr [x] 005641AB 89 45 D4 mov dword ptr [y],eax ;先赋值 005641AE 8B 4D E0 mov ecx,dword ptr [x] 005641B1 83 C1 01 add ecx,1 ;后自增 005641B4 89 4D E0 mov dword ptr [x],ecx 14: y = ++x; 005641B7 8B 45 E0 mov eax,dword ptr [x] ;先自增 005641BA 83 C0 01 add eax,1 005641BD 89 45 E0 mov dword ptr [x],eax ;会先写入x 005641C0 8B 4D E0 mov ecx,dword ptr [x] ;再读出来 005641C3 89 4D D4 mov dword ptr [y],ecx ;后赋值
-
单个if语句:
12: int x = 3, y = 5; 007945DA C7 45 E0 03 00 00 00 mov dword ptr [x],3 007945E1 C7 45 D4 05 00 00 00 mov dword ptr [y],5 13: 14: if (x>y) { 007945E8 8B 45 E0 mov eax,dword ptr [x] 007945EB 3B 45 D4 cmp eax,dword ptr [y] 007945EE 7E 0D jle main+6Dh (07945FDh) ;如果x<=y,则跳转到if的外面执行。否则继续向下执行。 15: printf("x>y"); 007945F0 68 DC 6B 79 00 push offset string "x>y" (0796BDCh) 007945F5 E8 94 CD FF FF call _printf (079138Eh) 007945FA 83 C4 04 add esp,4 16: 17: } 18: 19: 20: } 007945FD 33 C0 xor eax,eax
-
if-else语句:
12: int x = 3, y = 5; 00324CBA C7 45 E0 03 00 00 00 mov dword ptr [x],3 00324CC1 C7 45 D4 05 00 00 00 mov dword ptr [y],5 13: 14: if (x>y) { 00324CC8 8B 45 E0 mov eax,dword ptr [x] 00324CCB 3B 45 D4 cmp eax,dword ptr [y] 00324CCE 7E 0F jle main+6Fh (0324CDFh) ;如果x<=y,则跳转到else分支执行,否则继续向下执行。 15: printf("x>y"); 00324CD0 68 DC 6B 32 00 push offset string "x>y" (0326BDCh) 00324CD5 E8 B4 C6 FF FF call _printf (032138Eh) 00324CDA 83 C4 04 add esp,4 16: 17: } 00324CDD EB 0D jmp main+7Ch (0324CECh);执行完这个分支,要跳转到整个if的后面 18: else 19: { 20: printf("x<y"); 00324CDF 68 E0 6B 32 00 push offset string "x<y" (0326BE0h) 00324CE4 E8 A5 C6 FF FF call _printf (032138Eh) 00324CE9 83 C4 04 add esp,4 21: } 22: 23: 24: } 00324CEC 33 C0 xor eax,eax
-
从上面可以看到,jmp和jle都是短转移指令。只记录偏移量,方便重定位。
-
switch语句:
12: int x = 3, y = 5; 00FA4CBA C7 45 E0 03 00 00 00 mov dword ptr [x],3 00FA4CC1 C7 45 D4 05 00 00 00 mov dword ptr [y],5 13: 14: switch (x) 00FA4CC8 8B 45 E0 mov eax,dword ptr [x] 00FA4CCB 89 85 0C FF FF FF mov dword ptr [ebp-0F4h],eax;将变量从缓冲区的高地址区放到了低地址区。 00FA4CD1 83 BD 0C FF FF FF 03 cmp dword ptr [ebp-0F4h],3 ;case的比较是在最开始完成的。 00FA4CD8 74 0B je main+75h (0FA4CE5h) 00FA4CDA 83 BD 0C FF FF FF 04 cmp dword ptr [ebp-0F4h],4 00FA4CE1 74 11 je main+84h (0FA4CF4h) 00FA4CE3 EB 1E jmp main+93h (0FA4D03h) ;调到default执行。 15: { 16: case 3: 17: printf("x=3"); 00FA4CE5 68 DC 6B FA 00 push offset string "x=3" (0FA6BDCh) 00FA4CEA E8 9F C6 FF FF call _printf (0FA138Eh) 00FA4CEF 83 C4 04 add esp,4 18: break; 00FA4CF2 EB 1C jmp main+0A0h (0FA4D10h) ;跳到整个switch语句的最后 19: case 4: 20: printf("x=4"); 00FA4CF4 68 E0 6B FA 00 push offset string "x=4" (0FA6BE0h) 19: case 4: 20: printf("x=4"); 00FA4CF9 E8 90 C6 FF FF call _printf (0FA138Eh) 00FA4CFE 83 C4 04 add esp,4 21: break; 00FA4D01 EB 0D jmp main+0A0h (0FA4D10h) 22: default: 23: printf("no"); 00FA4D03 68 E4 6B FA 00 push offset string "no" (0FA6BE4h) 00FA4D08 E8 81 C6 FF FF call _printf (0FA138Eh) 00FA4D0D 83 C4 04 add esp,4 24: break; 25: } 26: 27: } 00FA4D10 33 C0 xor eax,eax
-
switch语句只能进行等值判断,if-else语句可以进行区间判断,switch的执行效率远远高于if-else。
-
快捷键的解析可以用switch判断。
-
当case的情况大于3个后,switch-case的汇编会发生变化,不再是逐个比对:
12: int x = 3, y = 5; 00144CBA C7 45 E0 03 00 00 00 mov dword ptr [x],3 00144CC1 C7 45 D4 05 00 00 00 mov dword ptr [y],5 13: 14: switch (x) 00144CC8 8B 45 E0 mov eax,dword ptr [x] 00144CCB 89 85 0C FF FF FF mov dword ptr [ebp-0F4h],eax 00144CD1 8B 8D 0C FF FF FF mov ecx,dword ptr [ebp-0F4h] 00144CD7 83 E9 03 sub ecx,3 ;计算x-3 主要是和所有case中最小的值比较。 00144CDA 89 8D 0C FF FF FF mov dword ptr [ebp-0F4h],ecx 00144CE0 83 BD 0C FF FF FF 03 cmp dword ptr [ebp-0F4h],3 ;比较x-3和3 00144CE7 77 49 ja $LN7+0Fh (0144D32h) ;如果x-3>3(这里是无符号的比较),跳转到default处执行。 00144CE9 8B 95 0C FF FF FF mov edx,dword ptr [ebp-0F4h] ;令edx为x-3。 00144CEF FF 24 95 80 4D 14 00 jmp dword ptr [edx*4+144D80h] ;去对应得内存地址查询要跳转到的地址。类似于中断向量表。 15: { 16: case 3: 17: printf("x=3"); 00144CF6 68 DC 6B 14 00 push offset string "x=3" (0146BDCh) 00144CFB E8 8E C6 FF FF call _printf (014138Eh) 00144D00 83 C4 04 add esp,4 18: break; 00144D03 EB 3A jmp $LN7+1Ch (0144D3Fh) 19: case 4: 20: printf("x=4"); 00144D05 68 E0 6B 14 00 push offset string "x=4" (0146BE0h) 19: case 4: 20: printf("x=4"); 00144D0A E8 7F C6 FF FF call _printf (014138Eh) 00144D0F 83 C4 04 add esp,4 21: break; 00144D12 EB 2B jmp $LN7+1Ch (0144D3Fh) 22: case 5: 23: printf("x=5"); 00144D14 68 E4 6B 14 00 push offset string "x=5" (0146BE4h) 00144D19 E8 70 C6 FF FF call _printf (014138Eh) 00144D1E 83 C4 04 add esp,4 24: break; 00144D21 EB 1C jmp $LN7+1Ch (0144D3Fh) 25: case 6: 26: printf("x=6"); 00144D23 68 E8 6B 14 00 push offset string "x=6" (0146BE8h) 00144D28 E8 61 C6 FF FF call _printf (014138Eh) 00144D2D 83 C4 04 add esp,4 27: break; 00144D30 EB 0D jmp $LN7+1Ch (0144D3Fh) 28: default: 29: printf("no"); 00144D32 68 EC 6B 14 00 push offset string "no" (0146BECh) 00144D37 E8 52 C6 FF FF call _printf (014138Eh) 00144D3C 83 C4 04 add esp,4 30: break; 31: } 32: 33: } 00144D3F 33 C0 xor eax,eax
-
switch函数的跳转表,存放在switch的编码范围内,即代码段中,不在栈中。跳转表中函数的地址=基址+偏移=0x00144d80(第一个case的地址)+edx*4 ,对于第一个case,当x=3时,应该执行基址的函数。所以edx应该为0,因此edx应该为x-3。
-
对于x>最大case的情况,在00144CE7处,就ja跳转到default。对于x<最小case的情况,计算的到的ecx是一个负数,当做无符号数看的话是一个非常大的数,通过ja也会跳转到default。而如果case出现空洞,其中的每个都会被填充为default的地址。
-
可见case的值只能是整数。
-
-
可以看到在0x001444d65处就ret了,后面的内容都是数据。所以把他们指令看起来会奇怪。最后还是有一个CC,用来防止错误执行到这里。
-
-
可以看到,当条件较多时,switch会在编译阶段根据各个case的值,安排好跳转的位置,放在一个连续的内存位置。
-
如果case的值是乱序的,那么函数地址表也是乱序的。如果case是有空当的,如果空当不大, 会填充default的地址,如果空当较大,则会在进行一层变化。
-
goto语句就等于jmp无条件跳转。
-
while语句:
12: int x = 1, y = 5; 00FD4CBA C7 45 E0 01 00 00 00 mov dword ptr [x],1 00FD4CC1 C7 45 D4 05 00 00 00 mov dword ptr [y],5 13: 14: while (x < 5) 00FD4CC8 83 7D E0 05 cmp dword ptr [x],5 00FD4CCC 7D 18 jge main+76h (0FD4CE6h) ;先判断,不符合条件,就不执行循环。 15: { 16: printf("x<5"); 00FD4CCE 68 DC 6B FD 00 push offset string "x<5" (0FD6BDCh) 00FD4CD3 E8 B6 C6 FF FF call _printf (0FD138Eh) 00FD4CD8 83 C4 04 add esp,4 17: x++; 00FD4CDB 8B 45 E0 mov eax,dword ptr [x] 00FD4CDE 83 C0 01 add eax,1 00FD4CE1 89 45 E0 mov dword ptr [x],eax ;写入内存,方便下一次比较。 18: } 00FD4CE4 EB E2 jmp main+58h (0FD4CC8h) ;无条件跳转。 19: 20: } 00FD4CE6 33 C0 xor eax,eax
-
do-while语句:
14: do 15: { 16: printf("x<5"); 00A74CC8 68 DC 6B A7 00 push offset string "x<5" (0A76BDCh) 00A74CCD E8 BC C6 FF FF call _printf (0A7138Eh) 00A74CD2 83 C4 04 add esp,4 17: x++; 00A74CD5 8B 45 E0 mov eax,dword ptr [x] 00A74CD8 83 C0 01 add eax,1 00A74CDB 89 45 E0 mov dword ptr [x],eax 18: } while (x < 5); 00A74CDE 83 7D E0 05 cmp dword ptr [x],5 00A74CE2 7C E4 jl main+58h (0A74CC8h) ;先执行最后判断。 19: 20: } 00A74CE4 33 C0 xor eax,eax
-
for循环,括号有三个部分,第1,3部分可以是没有结果的表达式,但是表达式2必须有结果。第1部分只会执行1次。:
14: for (int i = 0; i < 5; i++) 006E4CC8 C7 45 C8 00 00 00 00 mov dword ptr [ebp-38h],0 ;给i分配内存。 006E4CCF EB 09 jmp main+6Ah (06E4CDAh) ;跳转到判断i<5. 006E4CD1 8B 45 C8 mov eax,dword ptr [ebp-38h] ;每次循环结束后都要自增 006E4CD4 83 C0 01 add eax,1 006E4CD7 89 45 C8 mov dword ptr [ebp-38h],eax 006E4CDA 83 7D C8 05 cmp dword ptr [ebp-38h],5 ;然后再比较 006E4CDE 7D 0F jge main+7Fh (06E4CEFh) ;如果i>=5则退出循环。 15: { 16: printf("i"); 006E4CE0 68 DC 6B 6E 00 push offset string "i" (06E6BDCh) 006E4CE5 E8 A4 C6 FF FF call _printf (06E138Eh) 006E4CEA 83 C4 04 add esp,4 17: } 006E4CED EB E2 jmp main+61h (06E4CD1h) ;无条件跳转 18: 19: } 006E4CEF 33 C0 xor eax,eax
-
for循环中只有分号的情况。第二个部分如果是-1,则表示false。
-
-
数组空间的分配是在编译期间就做好的,因此数组的个数不能是变量:
9: void main() 10: { 00434150 55 push ebp 00434151 8B EC mov ebp,esp 00434153 81 EC D4 00 00 00 sub esp,0D4h ;堆栈提升了D4个字节,默认是C0个。相当于分配了20个字节。 00434159 53 push ebx 0043415A 56 push esi 0043415B 57 push edi 0043415C 8D BD 2C FF FF FF lea edi,[ebp-0D4h] 00434162 B9 35 00 00 00 mov ecx,35h 00434167 B8 CC CC CC CC mov eax,0CCCCCCCCh 0043416C F3 AB rep stos dword ptr es:[edi] 0043416E B9 03 A0 43 00 mov ecx,offset _1A512F26_test@c (043A003h) 00434173 E8 8B D0 FF FF call @__CheckForDebuggerJustMyCode@4 (0431203h) 11: int a[3] = {1,4,2}; 00434178 C7 45 F0 01 00 00 00 mov dword ptr [a],1 ;数组在栈中的顺序是小地址在前。 0043417F C7 45 F4 04 00 00 00 mov dword ptr [ebp-0Ch],4 00434186 C7 45 F8 02 00 00 00 mov dword ptr [ebp-8],2 12: 13: 14: 15: } 0043418D 33 C0 xor eax,eax
-
所有局部变量的声明都是在提升堆栈的时候就完成了。
-
利用数组越界修改函数的返回地址,这个程序会反复打印hhh\n:
#include <stdio.h> void fun() { while (1) { printf("hhh\n"); } } int check() { int a[8] = {1,2,3,4,5,6,7,8}; a[10] = (int)fun; //a[10]存放的本来是main函数中check()的下一句。check函数ret的时候就会跳到fun函数执行。 return 0; } void main() { check(); }
-
栈溢出:
void main() { check(); } int check() { int a[8] = {1,2,3,4,5,6,7,8}; a[10] = (int)main; //a[10]的位置存放的是main函数调用check前入栈的返回地址。因此执行完check函数后,ret会继续执行main函数。然后继续执行check。栈一直增长,直到溢出。 return 0; }
-
多维数组:
4: void main() 5: { 00964230 55 push ebp 00964231 8B EC mov ebp,esp 00964233 81 EC E0 00 00 00 sub esp,0E0h ;分配的内存和一维数组arr[6]一样 00964239 53 push ebx 0096423A 56 push esi 0096423B 57 push edi 0096423C 8D BD 20 FF FF FF lea edi,[ebp-0E0h] 00964242 B9 38 00 00 00 mov ecx,38h 00964247 B8 CC CC CC CC mov eax,0CCCCCCCCh 0096424C F3 AB rep stos dword ptr es:[edi] 0096424E B9 03 B0 96 00 mov ecx,offset _1A512F26_test@c (096B003h) 00964253 E8 B0 CF FF FF call @__CheckForDebuggerJustMyCode@4 (0961208h) 6: int arr[2][3] = { {1,2,3},{4,5,6} }; 00964258 C7 45 E4 01 00 00 00 mov dword ptr [arr],1 ;连续存放。 0096425F C7 45 E8 02 00 00 00 mov dword ptr [ebp-18h],2 00964266 C7 45 EC 03 00 00 00 mov dword ptr [ebp-14h],3 0096426D C7 45 F0 04 00 00 00 mov dword ptr [ebp-10h],4 00964274 C7 45 F4 05 00 00 00 mov dword ptr [ebp-0Ch],5 0096427B C7 45 F8 06 00 00 00 mov dword ptr [ebp-8],6 7: arr[1][2] = 9; 00964282 B8 0C 00 00 00 mov eax,0Ch ;从a[0]到a[1] 00964287 C1 E0 00 shl eax,0 0096428A 8D 4C 05 E4 lea ecx,arr[eax] ;ecx为a[1]的地址。 0096428E BA 04 00 00 00 mov edx,4 ;一个元素占4个字节 00964293 D1 E2 shl edx,1 ;偏移2个元素,即4*2个字节。 00964295 C7 04 11 09 00 00 00 mov dword ptr [ecx+edx],9 从a[1]到a[1][2] 。
-
多维数组的索引是用一维一维索引的方式来寻址的。他的出现是方便使用结构化的数据。
//在一个arr[2][3]的数组中,a[1][2]的内存编号为(int *)arr+3*1+2 int b = ((int *)arr)[3*1+2] == arr[1][2]; //结果为真。
-
二维数组在内存中的排列顺序:
a[2][3] a[0][0]→a[0][1]→a[0][2]→a[1][0]→a[1][1]→a[1][2] //是一行一行地存储的。
-
结构体类型的定义并不占用指令:
6: struct MyStruct 7: { 8: int a; 9: char b; 10: int c; 11: }; ;这段实际上是给编译器看的。 12: 13: struct MyStruct m1; ;局部变量,内存已经由堆栈安排好了。 14: m1.a = 4; 007F4D78 C7 45 F0 04 00 00 00 mov dword ptr [m1],4 15: m1.b = 5; 007F4D7F C6 45 F4 05 mov byte ptr [ebp-0Ch],5 ;占用一个字节。 16: m1.c = 7; 007F4D83 C7 45 F8 07 00 00 00 mov dword ptr [ebp-8],7 17: 18: 19: }
-
内存空间的安排,虽然b占用一个字节,但是c没有紧挨着b存储。
-
-
内存分配会按照变量的长度对齐,即地址号能够整除变量的长度。结构体的起始地址会根据最长变量的长度来进行对齐。同时对于结构体中的每个成员的偏移地址也会进行长度对齐。按照这种方式分配,CPU的读取效率高,这是以空间换时间。
-
7: struct MyStruct 8: { 9: char b; 10: int a; 11: }; ;占用8个字节,4+4 12: struct MyStruct2 13: { 14: char b; 15: __int64 a; 16: }; ;占用16个字节,8+8 ... 20: int a; 21: a = sizeof(struct MyStruct); 00BD1718 C7 45 F8 08 00 00 00 mov dword ptr [a],8 ;sizeof关键字在编译期间就生成结果了。 22: a = sizeof(struct MyStruct2); 00BD171F C7 45 F8 10 00 00 00 mov dword ptr [a],10h
-
用#pragma pack(n) 来让编译器改变结构体成员的对齐方式,不过不会改变结构体起始地址的对齐方式。n可以为1,2,4,8,VC编译器默认是8。不写时恢复默认。
#pragma pack(2) //从这往下,结构体内元素的偏移地址以2对齐。 struct MyStruct { char a; int b; char c; }; //占用2+4+2=8个字节 #pragma pack(4) //从这往下,结构体内元素的偏移地址以2对齐。 struct MyStruct2 { char a; __int64 b; char c; }; //占用4+8+4=16个字节 #pragma pack() //从这往下,结构体内元素的偏移地址以默认方式对齐(vc中是8)。
-
结构体的最后一个成员也要占用上足够的字节,假设存在下一个成员。
-
结构体数组是在内存中连续存放的。
-
指针类型只能做加减法,不能做乘除法,因为这个是给编译器看的。一个int *的指针+1后的地址,等于原地址+1*4。
-
指针可以比较大小(相当于做减法),是当做无符号数比较的:
5: int* a = (int *)100; 00AD1DF8 C7 45 F8 64 00 00 00 mov dword ptr [a],64h ;经过编译后,看不出是指针还是整形数据。 6: int* b = (int *)200; 00AD1DFF C7 45 EC C8 00 00 00 mov dword ptr [b],0C8h 7: if (a<b) { 00AD1E06 8B 45 F8 mov eax,dword ptr [a] 00AD1E09 3B 45 EC cmp eax,dword ptr [b] 00AD1E0C 73 0D jae main+4Bh (0AD1E1Bh) ;jae是无符号数比较的方法。 8: printf("a<b"); 00AD1E0E 68 CC 6B AD 00 push offset string "a<b" (0AD6BCCh) 00AD1E13 E8 58 F5 FF FF call _printf (0AD1370h) 00AD1E18 83 C4 04 add esp,4 9: } 10: 11: } 00AD1E1B 33 C0 xor eax,eax
-
取地址符 &只能对变量使用,不能对字面量使用,这个符号是给编译器看的。变量取地址的结果是指针类型。
-
取值符 *只能对指针类型使用。
-
lea指令:
5: int a = 4; 00233C82 C7 45 F4 04 00 00 00 mov dword ptr [a],4 6: int* p = &a; 00233C89 8D 45 F4 lea eax,[a] ;将地址a放入到eax中。 默认的[a]表示取出地址a中存放的值。 00233C8C 89 45 E8 mov dword ptr [p],eax
-
函数的参数传递,实际上是值传递:
-
9: int a = 4; 00E31718 C7 45 F8 04 00 00 00 mov dword ptr [a],4 10: fun(a); 00E3171F 8B 45 F8 mov eax,dword ptr [a] ;局部变量赋值给寄存器 00E31722 50 push eax ;将寄存器入栈。 00E31723 E8 5C FC FF FF call _fun (0E31384h) 00E31728 83 C4 04 add esp,4
-
数组名作为参数,传递是数组的起始地址,也就是第一个元素的地址:
9: int a[3] = {2,3,6}; 00EC1792 C7 45 EC 02 00 00 00 mov dword ptr [a],2 00EC1799 C7 45 F0 03 00 00 00 mov dword ptr [ebp-10h],3 00EC17A0 C7 45 F4 06 00 00 00 mov dword ptr [ebp-0Ch],6 10: fun(a); 00EC17A7 8D 45 EC lea eax,[a] ;实际传递的是数组的起始地址。 00EC17AA 50 push eax 00EC17AB E8 70 FB FF FF call _fun (0EC1320h) 00EC17B0 83 C4 04 add esp,4
-
数组作为参数时,一般也应传递数组的长度。避免在函数内使用越界。
-
字符串的三种使用方式:
6: char str1[6] = {'A','B','C'}; ;存储在栈上,逐个赋值。 00C81718 C6 45 F4 41 mov byte ptr [str1],41h 00C8171C C6 45 F5 42 mov byte ptr [ebp-0Bh],42h 00C81720 C6 45 F6 43 mov byte ptr [ebp-0Ah],43h 00C81724 33 C0 xor eax,eax ;如果数组还有空间,会用'\0'来填充。如果没有空间,则不会填充,有风险。 00C81726 66 89 45 F7 mov word ptr [ebp-9],ax 00C8172A 88 45 F9 mov byte ptr [ebp-7],al 7: char str2[] = "ABC"; ;存储在栈上,一次性赋值。编译器会自动在字符串的最后补0 00C8172D A1 30 7B C8 00 mov eax,dword ptr [string "ABC" (0C87B30h)] 00C81732 89 45 E8 mov dword ptr [str2],eax 8: char *str3 = "ABC"; ;字符串存储在常量区,栈中指针存储着地址。 00C81735 C7 45 DC 30 7B C8 00 mov dword ptr [str3],offset string "ABC" (0C87B30h) ;从常量区复制到栈中。 9: char *str4 = "ABC"; 00C8173C C7 45 D0 30 7B C8 00 mov dword ptr [str4],offset string "ABC" (0C87B30h) ;可以看到同一个字符串在内存中只保留一份,因为它存在于常量区,不会被修改。 10: str3[1] = 's'; 00C81743 B8 01 00 00 00 mov eax,1 00C81748 C1 E0 00 shl eax,0 00C8174B 8B 4D DC mov ecx,dword ptr [str3] 00C8174E C6 04 01 73 mov byte ptr [ecx+eax],73h ;执行这一条指令时,会报错,没有写入权限。
-
使用strcpy函数时,目标位置不能为静态区:
char str2[] = "ABC"; char *str3 = "CCC"; strcpy(str3, str2); //目标位置为静态区,会报访问权限错误。
-
strlen函数只是检查字节数,不是字符数,因此strlen("中")的结果为2。
-
使用strcat拼接字符串的时候,需要注意栈的覆盖:
char str1[6] = {'A','B','C','D','E','F'}; char str2[] = "ABC"; char* str3 = "CCCsssssssssssdasdasdas"; strcat(str2, str3);
-
strcat执行前内存分布:
-
-
strcat执行后内存分布:栈中的数组str1被修改了。
-
-
因此建议使用strcat_s版本,给定要追加的字符个数。如果源字符串是在栈上剩余空间不足或者是在常量区。可以考虑malloc一块内存,把源字符串和新字符串都用memcpy复制过去。
-
字符数组名不可以重新赋值(因为在内存中没有位置),但是字符指针可以重新赋值(在内存中有位置):
8: char str2[] = "ABC"; ;str2本质上是一个基址,编译器用来索引数组中的元素的,他在内存中没有位置。 00A54D30 A1 30 7B A5 00 mov eax,dword ptr [string "ABC" (0A57B30h)] 00A54D35 89 45 E8 mov dword ptr [str2],eax 9: char* str3 = "CCCsssssssssssdasdasdas"; ;str3存储的是常量区字符串的地址。 00A54D38 C7 45 DC D0 7B A5 00 mov dword ptr [str3],offset string "CCCsssssssssssdasdasdas" (0A57BD0h) 10: 11: str3 = "ss"; ;将str3中存储的内容修改为另一个字符串的地址。 00A54D3F C7 45 DC E8 7B A5 00 mov dword ptr [str3],offset string "ss" (0A57BE8h)
- 调用约定就是告诉编译器,如何传递参数,如何传递返回值,如何平衡堆栈。这些是由编译器决定的。
- 默认是使用栈传递参数,参数从右往左依次入栈,返回值通过eax传递。函数调用结束后,调用者移动esp平衡堆栈。
- 一般不建议修改,因为main函数使用的cdecl调用约定。
-
- 存在多种调用约定:
- __cdecl,C语言默认的调用约定。
- __stdcall,Windows api使用这种调用约定。参数从右往左入栈。通eax传递返回值。被调函数ret前自己进行堆栈平衡。
- __fastcall,第一和第二个参数使用ecx和edx传递,其余的从右向左依次入栈,eax传递返回值。速度最快。被调函数自己平衡堆栈。
-
定义函数指针的时候也可以指明调用约定,不同调用约定函数的不可以相互赋值:
int (__cdecl *fun)(int ,int);
-
利用函数指针来调用函数可以避免程序被下断点调试,相当于改变了函数名。
-
函数指针无法做加减运算,因为它本身没有宽度。
- 程序被编译后的目标代码,和链接后的可执行文件都是二进制的机器代码。反汇编就是将机器代码转化为人能够看懂的汇编代码。它和调试器不同,调试器是需要编译的时候加入符号信息,然后按行打断点,然后控制执行的,级别在高级语言层面。而反汇编是任意语言的。
- 要写反汇编器和汇编器,需要对x86指令架构非常熟悉。即机器码和汇编代码的相互转化。
- x86指令编码的结构:
-
- 前缀指令最多有4个,可以没有,没有顺序要求。因此分为4组,每组最多一个。
-
1. lock是用来锁地址总线的,主要是进行多核同步。 2. repne/repnz是当标志寄存器zero flag为0时,执行当前指令。否则不执行。rep/repz相反。 3. 当要进行内存读写时,如果没有特殊的段前缀指令,就表示从DS段读取。如果寻址出现了ebp和esp寄存器的话, 默认从SS段读取。 4. 32位保护模式下,CS段寄存器中的DB位,表示当前CPU工作在16还是32位。32位模式下,默认的操作数宽度就是32位。即寄存器使用eax,ebx而不是ax,bx。加上操作数宽度前缀指令,会切换一下操作数宽度。32↔16相互转化。地址宽度前缀指令的功能类似。
-
-
- opcode是指令的灵魂,即Operation Code操作码。1-3个字节。opcode决定了后面有没有modR/M,后者又决定了其后有没有SiB。
- 定长指令是指没有ModR/M,和SiB,而不是opcode是定长的。定长指令的opcode也可以是变化的。
- 一个字节有256种可能性。前缀指令占据了11个可能性。剩下245个是给opcode的。从下面的这个opcode映射表,查询50表示push rAX,其中r表示可以为RAX,EAX,AX。查询00,可以看到ADD Eb,Gb。这是一种Zz表示法,大写字母和小写字母分别表示,寻址方法和操作类型。如果第一个大写字母是E或者G,则表示当前指令有ModR/M。
- 下面的这个表格是同时包含了32位和64位的opcode表。可以看到对于40来说,INCi64表示这个指令在64位下是invalid,不合法的。REXo64表示该指令只在only64位下是合法的。下面的寄存器eAX REX则是分别对应上面提到的32位和64位。即opcode=40的功能在32位和64位下不同。
-
- 最常用的指令一般安排为一个字节的opcode。50-57是push通用寄存器,58-5F是pop通用寄存器。40-47是Inc通用寄存器,48-4F是dec通用寄存器。B0-B3是将一个字节送入AL,CL,DL,BL。B4-B7是将一个字节送入AH,CH,DH,BH。
- opcode=B0,表示将立即数移动到字节寄存器中。AL/R8B,Ib其中AL就是字节寄存器。R8B是64位情况下的,Ib指的是 immediate byte。这样的指令长度就是两个字节(不计算前缀的话),例如B3 F4 表示将F4整个立即数送入BL寄存器中。
-
- opcode=B8,表示将一个字或双字大小的立即数移动到字,双字或四字大小的寄存器中。v表示立即数的宽度取决于操作数的大小。32位模式下就是32位。例如32位下 B8 00F40211 表示将立即数1102F400送入EAX寄存器中。
-
- 91-97表示将指定的通用寄存器的值和rAx。opcode=91,在32位下表示将ecx和eax的值交换。90表示NOP,即什么也不操作,是用来对齐的。
-
- 可以用存在NOP来制作花指令,例如原本其他的代码是要跳转到77de01e8执行的add操作的,我们可以利用它上面的NOP,将该指令修改为B0,则指令add会前进一个字节,使得e7的位置构成一个mov al,0的指令,因此会改变原来指令的结构。识别花指令的方法是观察要跳转的地址是不是指令的起始地址。即如果jmp到77de01e8执行,但是发现e8不是指令的开始,就表示是花指令,将e8前面的修改为NOP即可还原。
-
-
- IP不像其他寄存器一样,可以用mov指令来修改。只能用jmp,call之类的间接修改。
- 70-7F表示条件短转移指令。opcode=72表示标志位寄存器的B为不为1时,才会跳转。B/NAE/C是不同的叫法,不同的反汇编引擎可能用不同的叫法,含义是相同的。短转移的意思就是偏移为1个字节 -128~127之间。因此指令的长度为2个字节(不考虑指令前缀)。f64表示在64位下,操作数宽度强制为64位。
-
- 708B表示当标志位寄存器O位为1时,跳转到相对于当前指令的下一条指令的起始地址偏移8B的位置。对于短转移来说,计算方法为D9+8B=164只保留64。因此实际地址为00418364。向回跳转了,8B是负的偏移地址。
-
- 如果要进行远跳转,即偏移地址范围为4个字节的有符号数,-2G~2G-1。opcode为两个字节,0F80-0F8F。
-
- LOOP系列指令,根据ecx的值确定循环次数。先对ecx减1,然后判断决定是否跳转。
-
- E8指令时调用函数,是长跳转。跳转到004283DE+1868FF6A=18AA8348
-
-
- EA指令表示长跳转,64位下无效。A表示地址直接被编码到指令中,而不是偏移。
-
- EA表示按照绝对地址进行跨段跳转,用在调用门中。32位情况下,地址长度为48位。高两个字节为CS,低两个字节为IP。
-
- C3是将栈顶元素的内容作为返回地址。用于外平衡栈模式。C2还带有两个字节的偏移。同样是将栈顶元素作为内容返回,但是栈顶会移动指定个字节。用作内平衡栈。
- ModR/M只有一个字节,分为3部分。
-
- 只要是指令的Zz说明中包含了E或G,则表示该指令有ModR/M。例如如下变长指令:
-
- 具体G表示那个寄存器,是由ModR/M中的第二部分,即3-5位确定的。一共有8种情况。
-
- 具体E表示那个寄存器还是内存单元,有ModR/M中的第1和3部分确定。
- 3-5位不仅可以用来标识寄存器,还可以作为opcode的扩展。
- SiB字节也分为3个部分,表示了一种复杂的寻址方式。
-
-
由于寄存器和内存的读写速度差距太大,所以一般一条指令中的两个操作数不会都在内存中。
-
Code Segment一般是存放代码,只读的。
-
Data Segment中的数据一般是静态数据,全局数据,在程序编译时就确定了位置和大小。
-
386处理器是首个32位的,把8086的8个通用寄存器都扩展成了32位,还增加了FS,GS两个段寄存器。
-
IA64特指Itanium安腾处理器的架构,不兼容IA32。被AMD64领先后,Intel推出了自己的兼容32位的64位架构Intel64。
-
64位的CPU除了扩展了原有10个通用寄存器,还增加了R8-R15一共8个通用寄存器。
-
对于1GHz的处理器,时钟周期是1ns,一般的指令执行大约需要几个时钟周期。
-
运算中的立即数是包含在指令编码中的。
-
常用的有两种汇编语言风格,不过他们对应的机器语言是相同的:
- Intel风格,有Intel发明的。
- AT&T风格,最早编写UNIX时发明的,由于UNIX在AT&T的贝尔实验室被发明而得名。比较容易地迁移到其他的UNIX系统上。
-
AT&T汇编和Intel汇编的区别:
+------------------------------+------------------------------------+ | Intel Code | AT&T Code | +------------------------------+------------------------------------+ | mov eax,1 | movl $1,%eax | | mov ebx,0ffh | movl $0xff,%ebx | | int 80h | int $0x80 | | mov ebx, eax | movl %eax, %ebx | | mov eax,[ecx] | movl (%ecx),%eax | | mov eax,[ebx+3] | movl 3(%ebx),%eax | | mov eax,[ebx+20h] | movl 0x20(%ebx),%eax | | add eax,[ebx+ecx*2h] | addl (%ebx,%ecx,0x2),%eax | | lea eax,[ebx+ecx] | leal (%ebx,%ecx),%eax | | sub eax,[ebx+ecx*4h-20h] | subl -0x20(%ebx,%ecx,0x4),%eax | +------------------------------+------------------------------------+
-
.globl main表示main标号是全局的,其他程序调用时,可以找得到。
-
gcc将main作为入口点,而as汇编器将_start作为入口点。
-
程序使用寄存器和内存是不需要通过内核的,而要使用各种I/O设备就需要和内核打交道,使用system call。
-
触发系统调用,需要提前设置功能号,放在寄存器eax,然后使用软中断 int 0x80。
-
32位环境下,4号中断是write,向一个文件描述符输出一段内存中的内容,目标文件描述符放在ebx中,起始地址放在ecx中,长度放在edx中。
-
-
#后面是注释。立即数前要$开头。寄存器前要加%。C语言中除非内嵌汇编,否则不能指定操作寄存器。
-
如果使用汇编语言直接调用系统调用,就可以不适用libc标准库,实际上标准库中也是使用的系统调用。
-
触发系统调用在32位下是int 0x80,在64位下是syscall指令。
-
局部变量放在栈中,在汇编语言中没有标记,只有全局变量会标记。标号相当于变量,可以使用movl $100, height将100放入到height标号的内存(4个字节)中。
.section .data msg: .ascii "This is a test message\n" factors: .double 37.45,12.30 height: .int 54 length: .int 62,35,47
-
以上全局变量的存储:
-
-
可以使用的数据类型:
-
-
立即数的表示:
值 立即数 10进制 125 $125 16进制 f8 $0xf8 8 进制 17 $017 2 进制 1001 $0b1001
-
对于一个4位2进制数1111,加1之后的结果为10000,产生进位存储不下了,CF置为1。
-
对于一个4位二进制数0111,加1之后结果为1111,没有进位,但是认为产生了溢出,因为0111当做有符号数是+7,而加1 后的结果当做有符号数来看是-8。溢出与否主要是看符号位,如果两个最高位为1(0)的数相加,结果的最高位变为了0(1)。那么就认为发生了溢出。
-
实际上CPU不知道参与运算的是有符号数还是无符号数。只是机械地进行按位的加法,每次计算完成后会设置相应的标志位。
-
进位的判断是由计算机对加法运算得出来的,加法是通过各位的全加器组合而来的。全加器的输出有本位和进位。最高位的全加结果如果有进位,则本次计算的CF置为1。
-
溢出的判断是如果操作数的符号位(A,B)相同,且和结果的符号位(C)不同,则认为发生了溢出。逻辑式为$(A\odot B)C'$。A和B同或,然后和非C相与。
-
bss是Block started by symbol的缩写,称为未初始化区域。data节是初始化了的区域。
-
#include <stdio.h> int a[1000]; //该部分会放在bss节,默认都会被初始化为0,因此在可执行文件中就标记一下这里有1000个int需要被初始化为0,不用在可执行文件中占据大小。 int b[1000]={1}; //该部分会放在data节中,执行时,直接映射到内存中,所以需要在可执行文件中占用实际的大小。 int main(){ printf("123\n"); return 0; }
-
从下图可以看出,使用默认值进行初始化的放在bss节,仅标记一下。使用自定义值进行初始化的放在data节,需要完整存放。
-
-
在data节中占据一块内容,在bss节中声明一块内容:.fill是重复的意思,默认是1个字节,值为0。
-
-
定义一个常量:
.section .data .equ LINUX_SYS_CALL, 0x80 #常量名,值 .section .text int $LINUX_SYS_CALL #使用常量,汇编器会自动替换
-
mov移动指令,如果有一个操作数是寄存器,那么就不用显式指出操作数的长度,否则需要使用movb,movw,movl,movq。操作数的长度需要相同。一般只有将立即数移动到内存的时候才需要指明长度。
-
通用寄存器(32位下8个,64位下16个)之间可以相互赋值。通用寄存器和特殊寄存器相互赋值,特殊寄存器之间不能相互赋值。EIP和EFLAGS不可以被赋值。
-
内存偏移量的表示方法:
.section .data values: .int 10,20,30,40,50 .section .text mov $2, %edi mov values(,%edi,4), %eax #获取values变量后面8个字节的值,即第二个元素,30 ,组合后的地址为values+0+%edi*4
-
-
其中offset_address和index必须是寄存器。size可以是立即数。0可以不写,但是逗号不能省略。同时改变offset和index可以进行二维数组的索引。
-
地址和指针,下面的括号寻址和上面的内存偏移量是一致的:
mov values, %eax #将变量values的值赋值给寄存器eax。 mov $values, %eax #将变量values的地址赋值给寄存器eax。 mov $100, %ecx #将一个立即数赋值给寄存器eax。 mov %ebx, (%eax) #将寄存器ebx的值赋值给eax寄存器指向的内存单元。也就是赋值给变量values。 mov $99, 4(%edi) #
-
XCHG 指令交换连个操作数的值。寄存器和寄存器,寄存器和内存。不允许内存和内存。使用这个指令可以简化操作。由于所有的指令都是原子的,所以不会被打断。多进程或多线程共享资源时,需要考虑使用锁来防止同时修改一个内容,这就是需要指令级别的支持。
-
LOCK=1表示上锁,LOCK=0表示未上锁。
- 如果现在lock=1,那么在执行完xchg后中断,另一个程序来取锁,得到的也是1。都认为被锁住了。
- 如果现在lock=0,那么在执行完xchg后中断,lock变成了1,另一个进程来取锁,得到的是1,认为被锁住了。原来的进程检查发现刚才交换得来的lock=0,即未加锁,可以使用。
- 如果要释放资源,就将lock置为0即可。
-
-
CMP指令使用第二个操作数-第一个操作数。ADD是将第一个操作数加到第二个操作数上。
-
sub a,b 的结果是b= b-a。cmp a,b 是只做减法,不写入,但是会修改标志寄存器。
-
neg a 是将操作数替换为他的2补数,即按位取反,再加1。这一操作对于有符号数来说就是取相反数,对于无符号数来说是求对称数。例如4位2进制无符号数3(0011)的2补数为13(1101),而这关于1000对称。如果操作数为0,则CF会定会被置为0,否则为1。
-
inc dec指令不修改CF。减法操作可以变成取负数和加法操作的结合。
-
无符号数乘法 mul source 指令中只有一个操作数,另一个操作数在寄存器al,ax等中。如果高位的寄存器不为0,即使用到了高位寄存器,则OF和CF会被置为1。
-
-
有符号数乘法有三种形式:
imul source #另一个乘数和结果的存放位置和无符号数的相同。 imul source, destination #两个操作数长度相同,可能会放不下。 imul multiplier, source, destination #multiplier必须是立即数。
-
有符号和无符号的触发都是一个操作数div source和idiv source。divdend/divisor=quotient...reamainder
-
-
编译器会将乘除法优化为移位运算,左移的情况算术和逻辑是相同的,右侧补零即可:
sal destination #算术左移,1位 sal shifter, destination #向左移动shifter位 shl ... #逻辑左移 #算术移位一般是对有符号数用,逻辑移位一般为无符号数用 sar destination #算术右移1位,左侧补原来的最高位 shr destination #逻辑右移1位,左侧补0
-
左移的那一位会进入到CF位。对于有符号和无符号数,最高位为1时,左移1位都会发生进位。
-
右移的那一位会进入CF位。相当于除2的余数。
-
循环移位:
rol destination #循环左移,右侧补充左侧的那一位。 ror .. #循环右移,左侧补充右侧的那一位。 rcl .. #带进位的循环左移,最高位进入CF,右侧替换为原来的CF位。 rcr .. #带进位的循环右移,最低位进入CF,左侧替换为原来的CF位。
-
逻辑操作:and / or /xor source, destination 结果写入到destination
-
test source destination 等价于and,但是不写入目标寄存器,只设置状态寄存器。
-
not destination 按位取反。 neg 指令相当于not 再inc
-
修改EIP有三种方法:jmp,call,Interrupt。
-
有符号数和无符号数的比较都是用cmp指令,但是跳转使用的是不同的指令。
-
cmp的比较就是做减法,减法是不区分有符号和无符号数的。减法的进位实际上就是借位。
-
对于无符号数来说,如果A-B不发生借位,则表示A>=B,如果ZF=0,即结果不为0,则表示A>B。
-
对于有符号数来说,如果A-B没发生溢出(OF=0),那么结果为非负(SF=0)表示A>=B,如果A-B发生了溢出(OF=0),那么结果为负(SF=1)表示A>=B。总的来说当SF=OF时A>=B。
-
-
1补数(1’s complement)和2补数(2’s complement):一个数的1补数就是按位取反,一个数的2补数就是1补数+1,例如:
1补数 0111→1000 1100→0011 2补数 0111→1001 1100→0100
-
32位下POSIX系统调用和64位的不同,例如32位下1号系统调用为exit,64下1号系统调用为write。
-
32位下使用int 0x80,软中断,触发系统调用,64位下使用syscall指令触发系统调用。
-
触发系统调用会将相关寄存器的值传递过去。
-
32位下eax存储系统调用号,参数依次存放在ebx,ecx,edx,esi,edi。系统调用的返回值存储在eax中。
-
64位下rax存储系统调用号,参数依次存放在rdi,rsi,rdx,r10,r8,r9。系统调用的返回值存储在rax中。
-
System V AMD64调用约定:当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。当参数为 7 个及以上时, 前 6 个与前面一样, 但后面的依次从 "右向左" 放入栈中。系统调用的参数最多是6个,不会使用到栈,速度快。
-
64位系统中,可以执行32位的系统调用。
-
32程序默认使用栈来函数调用需要的传递参数。
-
在64位汇编中使用libc:
.section .data output: .asciz "Hello %s\n" name: .asciz "World" .section .text .globl main main: mov $output, %rdi mov $name, %rsi mov $0, %rax #因为printf是变参的,需要设置这个。 call printf #参数较少时,通过寄存器传参。C函数原型为printf("Hello %s\n","World"); push $0 call exit
-
在32位汇编中使用libc,编译时要加上-m32选项:
.section .data output: .asciz "Hello %s\n" name: .asciz "World" .section .text .globl main main: pushl $name pshl $output #32位的参数 call printf #参数较少时,通过寄存器传参。C函数原型为printf("Hello %s\n","World"); addl $8,%esp #使用栈传参,要平栈。使用寄存器传参不用。 push $0 call exit
-
64系统中默认不会安装32位的开发环境,但是可以运行32位程序。
-
lea指令将标号的地址存入寄存器:
lea value,%esi #等价于mov $value,%esi
-
movsb,movsw,movsl 指令,将esi指向的内容复制到edi指向的内容。esi和edi是source和destination的缩写。没有操作数。esi和edi也会跟着movs指令移动。指针移动的方向取决于DF标志位,DF=0,地址递增,否则递减。可以使用cld设置DF=0,std设置DF=1。不过该命令在移动多个字节时,本身是从低→高的。
-
rep前缀,放在任何指令前,表示该指令要重复,每次循环前ecx-1。省略了loop。该指令不会被打断。
-
lodsb,lodsw,lodsl 指令将esi指向的内容复制到al,ax,eax中。也会移动esi,取决于DF。stos?也有系列指令。
-
cmps?系列指令用来比较esi和edi指向的内容。相当于(%esi)-(%edi)。
-
定义函数:
.type area,@function #将标号area当做函数。 area: ret .globl main main: call area
-
形参parameter 实参argument
-
C调用约定使用栈来传递参数,可以用4(%esp)来取出第一个参数,但是由于%esp会随着程序的执行而变化,所以使用一个%ebp栈底指针来记录刚进入被调函数后的%esp。而ebp之前也是有值的,需要入栈保存一下。
func: push %ebp #保存上一个栈底指针 mov %esp, %ebp .. #可以用8(%ebp)来获取第一个参数 sub $12,%esp #在被调函数的栈上开辟12个字节,供局部变量使用。 mov %ebp, %esp pop %ebp ret .globl main main: push %ecx #保存所有有用的寄存器 pushl $2 pushl $5 pushl $8 call func #函数原型为func(8,5,2) addl $12, %esp #调用者平衡栈。 pop %ecx #逆序出栈,恢复寄存器
-
因此3个实参的函数调用如下:
-
-
如果要想将一个函数独立成一个.s文件,那么就需要使用.globl修饰该文件内的函数标号,这样链接时就可以找到该函数了。
-
控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性。32位下寄存器为32位,第0位PE(Protected-Mode Enable)为0表示CPU处于实模式下,为1表示CPU处于保护模式下,并使用分段机制。第31位PG(Paging Enable)为0表示启动了分页机制,为1表示没有启动。
-
在C中嵌入汇编最大的问题就是如何将C中变量和汇编中的寄存器联系起来。
-
基本汇编的格式:
asm("movl %ecx, %eax"); /* 将 ecx 寄存器的内容赋值给 eax */ __asm__("movb %bh, (%eax)"); /* 将 bh寄存器的(大小为一个字节)赋值给 eax 寄存器指向的内存 */ //asm和__asm__都是可以的,后者是为了防止asm被使用为标识符。如果指令有多条,可以每条一行,用""括起来,末尾用\n\t分隔。 __asm__ ("movl %eax, %ebx/n/t" \ #换行使用\接续。 "movl $56, %esi/n/t" \ "movl %ecx, $label(%edx,%ebx,$4)/n/t"\ "movb %ah, (%ebx)");
-
基本汇编修改寄存器,但是编译器不知道这些寄存器被修改了。而且基本汇编无法操作C中的变量,只能操作寄存器,功能有限,因此出现了扩展汇编。
-
扩展汇编允许指定输入操作数、输出操作数以及修饰寄存器列表。
__asm__ __volatile__ ( 汇编程序模板 /*必须有*/ : 输出操作数 /* 可选的 */ : 输入操作数 /* 可选的 */ : 修饰寄存器列表 /* 可选的 */ ); __volatile__ 表示编译器不要优化代码,后面的指令保留原样。一般用在位置必须固定的汇编代码。也可以用volatile
-
汇编语句模板由多条汇编指令组成,每条指令用""括起来,指令用分号或\n\t结尾。如果一共有n(0-9)个操作数,第一个输出操作数用%0引用,最后一个输入操作数用%n-1。默认当做long类型(4个字节)来看待。可以用%h1或%l1来使用高16位和低16位。
-
每一个操作数由一个操作数约束字符串所描述,其后紧接一个 ( ) 括起来的C表达式。例如"c" (count) 操作数之间用逗号分隔。输出操作数的约束字符串以=开头。约束字符串主要用于决定操作数的寻址方式,同时也用于指定使用的寄存器。
-
输出操作数表达式必须为左值。输入操作数的要求不像这样严格
-
asm ("cld/n/t" #设置指针移动的方向为地址递增。 "rep/n/t" #重复下一条指令ecx次,每次都会-1。 "stosl" #将eax寄存器的内容复制到edi指向的内存(4个字节)中,完事edi+4。 : /* 无输出寄存器 */ : "c" (count), "a" (fill_value), "D" (dest) : "%ecx", "%edi" #告诉gcc,寄存器ecx和edi一直无效 ); int a=10, b; asm ("movl %1, %%eax;" #操作数1个%,寄存器用2个% "movl %%eax, %0;" :"=r"(b) /* 输出 */=表示这个是操作数时输出的。r表示gcc可以用任意一个寄存器来存储。 :"r"(a) /* 输入 */ :"%eax" /* 修饰寄存器 */ );
-
操作数例子:
asm ("leal (%1,%1,4), %0" : "=r" (five_times_x)#r任意通用寄存器GPRs,由GCC决定。leal的%0会被替换为一个寄存器而不是内存。 : "r" (x) #由于lea指令,括号中前两个操作数必须在寄存器中,因此x会被移动到一个GCC决定的通用寄存器中。 ); asm ("leal (%0,%0,4), %0" : "=r" (five_times_x) : "0" (x) #0表示和前一个操作数使用同一个寄存器。 ); asm ("leal (%%ecx,%%ecx,4), %%ecx" : "=c" (x) #将变量x存入ecx寄存器,最后将ecx寄存器的值写会变量x。 : "c" (x) #将变量x存入ecx寄存器 );
-
修饰寄存器列表:一些指令会修改寄存器的内容,在修饰寄存器列表中列举出来,。不用在此列出输入和输出寄存器,因为gcc知道他们已经被使用。如果我们的指令可以修改条件码寄存器(cc),我们必须将 "cc" 添加进修饰寄存器列表。如果我们的指令以不可预测的方式修改了内存,那么需要将 "memory" 添加进修饰寄存器列表。这可以使 GCC 不会在汇编指令间保持缓存于寄存器的内存值。
-
asm ("movl %0,%%eax; movl %1,%%ecx; call _foo" #_foo函数接受两个参数,存储在eax和ecx中。 : /* no outputs */ : "g" (from), "g" (to) : "eax", "ecx" #告诉GCC保护好这两个寄存器。 );
-
寄存器操作数约束,当时用如下约束时,操作数将会存储到寄存器中,指令中的%引用,都将会替换为对应的寄存器:
+---+--------------------+ | r | Register(s) | +---+--------------------+ | a | %eax, %ax, %al | | b | %ebx, %bx, %bl | | c | %ecx, %cx, %cl | | d | %edx, %dx, %dl | | S | %esi, %si | | D | %edi, %di | +---+--------------------+
-
内存操作数约束m,表示对操作数的操作就发生在内存中。一个变量既可以做输入参数,也可以做输出参数。
asm ("incl %0" :"=a"(var):"0"(var)); #这里的 "0" 用于指定与第 0 个输出变量相同的约束。var 输入被读进 %eax,并且等递增后更新的 %eax 再次被存储进 var。
- 最简单的逻辑门可以用二极管或三极管制成。
- 一位半加器:只能做一位的加法,且不考虑输入的进位。S为本位,C为进位。
-
-
$S=A\otimes B$ ,$C=AB$。 -
- 多位二进制数相加,最低位的加法是没有进位的,可以用半加器实现。
- 一位全加器,考虑低位的进位。先不考虑进位计算,再加上进位的值。例如计算:1+1+1,先计算第一个1+第二个1得到本位为0,进位为1。在将上一次的本位0和第三个1相加,得到本位为1(最终的本位),再计算上一次的本位0+输入的进位1,发现没有进位,最终的进位取决于两次后两次加法是否有进位,只要一个有进位,最终就有进位。
- 三个二进制加法的进位和十进制区别在于三个十进制加法的进位情况有三种:没有进位(1+1+1),1次进位(6+6+6),2次进位(8+8+8),无法用一个二进制位来表示进位与否。
-
-
- 四位二进制全加器,采用串行(后一位的计算必须要等到前一位计算完成再开始。)进位。两个数分别为$A_3A_2A_1A_0$和$B_3B_2B_1B_0$。
-
- 加减法器:可以将最低位的输入进位当做控制位,为0时表示要做加法,为1时表示要做减法。做减法时,应该讲B取反加1。而加1可以用加控制位来代替。所以只要取反即可。
-
- 溢出只发生在正数+正数还有负数+负数。正数+正数(A[3]和NB[3]都为0)一定不会进位,Cout=0,如果次位结果有进位,说明发生了溢出。负数+负数(A[3]和NB[3]都为1)一定会进位,cout=1,如果次位加法没有进位,那么S[3]=0,说明发生了溢出。
-
- 为了克服串行进位加法器速度慢的缺点,开发出了超前进位加法器,可以根据每一位的待机算数直接判断出进位与否。电路复杂度换时间。
-
- 改进全加器运算公式的形式:
-
- 展开递推式,可以建立$A_i,B_i,C_0$和$S_i,C_i$的关系。可以看出来,有些部分被重复计算了。
-
- 使用加法器实现减法器。$A-B$等价于$A+B_补$。正数的补码就是它本身,负数的补码是按位取反再加1。
- 可以利用多路复用器来实现移位的功能。
- RISC的十种基本逻辑运算:
-
- ALU的基本结构,两个运算输入,一个控制输入opcode,一个结果输出。
-
- 寄存器是用具有记忆功能的触发器构造的。
- RAM有两种,SRAM是静态的,由纯晶体管构成,常用于缓存器, 体积也较大。DRAM是动态的,内部有电容和晶体管构成,读取和写入都要电容的充放电,速度稍慢。