汇编语言学习

前序

    因为今年想转向二进制安全,感觉毕业一定要有个方向,这样好投递岗位,所以学习汇编语言是第一站,在学校的智云课堂上,找了老师的课来上,讲的特别好,老师不让传任何相关资源到网上,我在这里做一个对汇编学习简要的总结,我之后逆向或者搞二进制漏洞的时候,也可以方便查阅,虽然网上一搜都是,这里写一下,也算是巩固一下自己学的东西,以后回头看的时候,能够快速弄懂。

环境

    我的编程环境在xp虚拟机中,使用masm进行编译,连接,td(Turbo Debugger)来进行调试,安装教程在这里:https://www.jianshu.com/p/43c8661b381d, 我没进行尝试,反正我是直接用的老师的打包好的xp虚拟机,贼方便:)
    基本调试环境如下图所示,我最常用的几个操作如下:

  • F2设断点
  • F7跟踪进入(trace into)
  • F8单步执行(step over, 不进函数)
  • F9直接运行,会在断点断下
  • 在寄存器窗口右键,能转换成32位寄存器,因为这个环境也能写32位汇编程序,就是偏移得用16位
  • 在各个cs,ds,ss窗口点击Ctrl+G,输入ds:0587,就能到对应的全局变量地址
  • window下有个User Screen,能看到当前黑窗口内输出了什么
    在这里插入图片描述

基本知识

寄存器

16位cpu的寄存器有
ax, bx, cx, dx(前四个寄存器加减乘除,逻辑运算,二进制运算,其中bx可用作偏移,其他都不能)
其中上面四个都可以再划分,比如ax,可以分为al(低8位)和ah(高8位)
sp, bp, si, di(sp,bp是栈寄存器,si,di负责偏移和计算)
cs, ds, es, ss(段地址寄存器,其中es:extra segment附加段,它跟ds类似,可以用来表示一个数据段的段址)
ip, fl (两个寄存器无法被用户直接修改,可以采用一些间接方法进行修改)
在这里插入图片描述
上图比较清晰的讲解了寄存器,图片来源于网上,https://blog.csdn.net/cqkxboy168/article/details/8994479
32位cpu的寄存器有
eax, ebx, ecs, edx
esp, ebp, esi, edi
cs, ds, es, ss(这里的段寄存器是16位,其他都是32位)
eip, efl

堆栈

    网上找了一个解释堆栈的图:

  • 最上面是栈(stack),一般用来保存局部变量,有 8 MB 的大小限制,因此不建议在函数内开大数组,递归的效率低是因为容易栈溢出。栈的增长方向是向下的。
  • 堆(heap),动态分配的内存会在这里处理,例如 malloc、new。堆是向上增长的。
  • data 区,静态存储区,存放全局变量,静态变量,常量等。
  • text 区和共享库,是可执行机器指令,是只读的。

在这里插入图片描述
    在汇编语言中,ss: stack segment(用来表示堆栈的段地址),sp:stack pointer(堆栈指针, 表示堆栈顶端的偏移地址), 但是不能用[sp]或[sp+常数或其它寄存器]的形式来引用某个变量。可以使用[bp+常数或其它寄存器]的形式来引用某个变量。

汇编代码结构

;16位汇编语言程序
data segment
;这里定义全局变量(数据)
data ends
code segment
;assume的作用
;帮助编译器建立段寄存器与段的关联, 当源程序中引用了某个段内的变量时,编译器会在编译出来的机器码中把变量的段地址替换成关联的段寄存器
assume cs:code, ds:data
main:
   mov ax, data
   mov ds, ax

code ends
end main ;表示开始地址为main

hello world

;变量的偏移地址就是该变量离它所在段开端的距离 
data segment
hello db "Hello$world!", 0Dh, 0Ah, 0
data ends
code segment
assume cs:code, ds:data ;这里是在将两个部分段地址给cs和ds
main:
   mov ax, data
   mov ds, ax  ;mov ds, data是不允许的,需要通用寄存器来转乘一下
   mov si, 0
next:
   mov dl, hello[si] ;经过编译后变成mov dl, ds:[3+si]
   cmp dl, 0
   je exit
   mov ah, 2
   int 21h
   add si, 1
   jmp next
exit:
   mov ah, 4Ch ;这里是调用21号函数库的4C号函数,目的是结束程序
   int 21h
code ends
end main

地址

  • 编程的时候只能使用逻辑地址,不能使用物理地址
  • 段起始地址的16进制个位必须=0, 否则不能成为段起始地址(将末尾0去掉就是段地址)
  • 在指令中必须用段寄存器:[变量的偏移地址]这种搞形式来引用变量。在源程序中,[]中可以用变量名来表示变量的偏移地址。例如ds:[123]

1、直接寻址

  • 用常数来表示变量的偏移地址
  • 例如,ds:[2000h]

2、间接寻址

  • 用寄存器、寄存器+常数来表示变量的偏移地址, 只能使用这四个BX, BP, SI, DI寄存器
  • 例如,ds:[bx]
  • 32位比16位多了以下这种寻址方式(可以做乘法) :[寄存器+寄存器*n+常数], 其中n=2、4、8。

3、引用数组元素

  • 假设存在全局变量p1n9 db 0Dh, 0Ah, ‘$’
  • [p1n9]可以理解成地址p1n9所指向的对象。mov ah, [p1n9]的完整形式其实是: mov ah, byte ptr ds:[p1n9] 其中ds是变量p1n9的段地址
  • 若[]中不包含寄存器bp,则该变量默认的段地址一定是ds,则引用data段内的元素,可不加ds
  • 汇编语言的语句中,如果源操作数或目标操作数的其中之一有明确的类型即宽度,则另外一方不需要指定类型。例如这种mov [p1n9], 0,就不知道0的宽度是多少,需要指定宽度,正确用法是mov byte ptr [p1n9], 0

标志位

FL共16位, 但只用其中9位,这9位包括6个状态标志和3个控制标志,如下所示:
11 10 9 8 7 6 4 2 0(这里是FL寄存器第几位表示什么)
O D I T S Z A P C
标志位讲解

  • CF:进位标志位。在无符号运算时,记录了运算结果的最高有效位向更高位的进位值或从更高位借位,产生进位或借位时CF=1,否则CF=0;
  • PF:奇偶标志位。相关指令执行后结果所有bit中1的个数为偶数,那么PF=1,1的个数为奇数则PF=0;
  • AF:辅助进位标志位。运算过程中看最后四位,不论长度为多少。最后四位向前有进位或者借位,AF=1,否则AF=0;
  • ZF:零标志位。相关指令执行后结果为0那么ZF=1,结果不为0则ZF=0;
  • SF:符号标志位。相关指令执行后结果为负那么SF=1,结果非负数则SF=0;
  • TF:调试标志位。当TF=1时,处理器每次只执行一条指令,即单步执行;CPU在每执行完一条指令后,会自动在该条指令与下条指令之间插入一条int 1h指令并执行它。
  • IF:中断允许标志位。它用来控制8086是否允许接收外部中断请求。若IF=1,8086能响应外部中断,反之则屏蔽外部中断;cli指令使IF=0表示关/禁止硬件中断; sti指令使IF=1表示开/允许硬件中断
  • DF:方向标志位。在串处理指令中,每次操作后,如果DF=0,si、di递增,如果DF=1,si、di递减;注意此处DF的值是由程序员进行设定的 cld命令是将DF设置为0,std命令是将DF设置为1;
  • OF:溢出标志位。记录了有符号运算的结果是否发生了溢出,如果发生溢出OF=1,如果没有OF=0;

条件跳转指令可以分为四个类型:

跳转参考标志位

  • jb: CF=1, 故jb≡jc
  • ja: CF=0 且 ZF=0
  • jg: SF==OF 且 ZF=0
  • jge:SF==OF
  • jl:SF!=OF
  • jle: SF!=OF || (SF==OF && ZF=1)

变量定义

  • db, dw, dd, dq, dt
unsigned char      == 汇编的byte(字节) 8unsigned short int == 汇编的word() 16unsigned long int  == 汇编的double word(双字)32位
a db 12h; unsigned char a = 0x12;
b dw 1234h; unsigned short int b=0x1234;
c dd 12345678h; unsigned long int c=0x12345678;

byte ptr ; 1字节
word ptr ; 2字节
dword ptr; 4字节(32位整数或float类型小数)
fword ptr; 6字节(4字节偏移地址+2字节段地址)
qword ptr; 8字节(64位整数或double类型小数)
tbyte ptr; 10字节(long double类型的80位小数)

小端规则

设a的地址为1000, 则a的值在内存中的布局如下所示:
地址 值
1000 0x34; 低8位在前
1001 0x12; 高8位在后
数据在内存中的存放规律: 低字节在前,高字节在后

零扩充和符号扩充

当把一个宽度较小的值赋给宽度较大的变量时,会发生扩充。
扩充包括零扩充及符号扩充两种。
对于符号数,正数左侧补0,负数左侧补1
对于非符号数,左侧均补0

定义函数

//用标号来定义函数的名称
fun:
  ;操作
  ret
main:
  mov ax, 1
  call fun
  ;exit
  mov ah, 4Ch
  int 21h

指令用法

算术运算

word ptr相当于C语言中的short int *,其中ptr是pointer的缩写。
byte ptr相当于C语言中的char *
dword ptr相当于C语言中的long int *
所有算术运算一定要等宽,不等宽就会报错

  • add 加
add 寄存器,寄存器
add 寄存器,常数 
add 寄存器,变量
add 变量, 寄存器
add 变量, 常数
注意不能add 变量, 变量
最后的结果存储在左边的操作数中
  • sub 减subtract(和上面add是一样的)
  • mul 乘multiply
mul是非符号数的乘法指令
imul是符号数的乘法指令

在这里插入图片描述

  • div 除divide、求余
div是非符号数的除法指令
idiv是符号数的除法指令
(1) 16位除以8位得8位
ax / 除数 = AL..AH
例如: div bh
设AX=123h, BH=10h
div bh; AL=12h, AH=03h
(2) 32位除以16位得16位
dx:ax / 除数 = ax..dx
例如: div bx
设dx=123h, ax=4567h, bx=1000h
div bx  ; 1234567h/1000h
; AX=1234h, DX=0567h
(3) 64位除以32位得32位
edx:eax / 除数 = eax..edx
例如: div ebx
假定要把一个32位整数如7FFF FFFFh转化成十进制格式
则一定要采用(3)这种除法以防止发生除法溢出。

当发生除法溢出时,有两种情况,一是除以0,另一个就是商无法用对应的寄存器存储,当发生溢出时,系统会自动插入00的中断函数,会显示溢出信息并终止程序运行

  • XLAT (translate) 也称查表指令
在xlat执行前必须让ds:bx指向表, al必须赋值为数组的下标; 执行xlat后, AL=ds:[bx+AL]
设ds=数组的段地址
mov bx, offset t; BX=表的首地址
mov al, 2; AL为下标
xlat
xlat指令要求DS:BX指向数组,AL=数组下标。
执行指令后, AL=数组元素
  • inc指令,自增1,inc ax,ax=ax+1,inc不影响CF位, add指令会影响CF
  • adc指令: add with carry 带进位加,例如:adc dx, 05h === dx = dx + 05h + CF
  • dec指令,与inc差不多,dec指令不影响CF
  • neg:negate 求相反数, 会影响CF,ZF,SF等标志位。-x ≡ ~x + 1
  • sbb: subtract with borrow 带借位减, 例如:sbb dx, 05h === dx = dx - 05h - CF
  • cmp指令其实只是做了减法影响了标志位,例如:cmp ax, bx; 内部是做了减法ax-bx,但是抛弃两数之差,只影响标志位。

逻辑运算

  • and &,test指令功能与and一样,但是test不会修改左操作数,只会影响标志位
  • or |
  • xor ^
  • not ~
  • shl <<
  • shr >>
  • rol: rotate left 循环左移
  • ror: rotate right 循环右移
.386以上cpu中, 移位次数大于1时也可使用常数
但是在16位操作系统时,移位次数等于1,使用rol ax,1,而大于1,则应该用mov cl,4  rol ax,cl,两条指令来实现
xp中这样写就可以使用32位寄存器了

.386(代表是32位程序)
data segment use16(声明这里使用16位偏移)
data ends
code segment use16
code ends

和算术运算寄存器一样,结果存储在左边的寄存器中
  • sal和sar是针对符号数的左移和右移,sar右移时负数左边补1,正数左边补0,sal与shl一样,右边均补0
  • rcl: rotate through carry left 带进位循环左移
  • rcr: rotate through carry right带进位循环右移,这两个移动就是每次会将CF看成数的最高位,带着CF一起移动

通用数据传送指令

  • MOV 例如: mov ax,1, 是从右向左赋值的,linux中是从左向右赋值的
  • PUSH
  • POP
  • 注意:push/pop后面不能跟一个8位的寄存器或变量
  • XCHG(交换,寄存器与寄存器,寄存器与变量),XCHG ax, bx,这样两个寄存器的值就交换过来了
  • 注意:两个操作数不能同时为内存变量
  • 还有两个特殊的指令,pushf/popf配合起来除了可以刻意改变FL中的某些位外,也可以用来保护/恢复FL的值;

地址传送指令

  • lea dest, src(取变量的偏移地址),例如:lea ax,ds:[1000h],那么ax=1000h,和mov ax,offset p1n9, 这个p1n9变量的偏移相对于段地址就是1000h
  • 远指针(far pointer)包括段地址及偏移地址两个部分;48位的远指针在汇编语言中有一个类型修饰词: fword ptr
在32位系统下,16位的段地址不再通过后面补一个十六进制的0来得到段首地址,而是要通过查表得到段首地址。
32位系统有个gdt表,gdt表其实是一个数组,该数组的首地址存放在gdtr寄存器(获得该表位于何处)
  • 近指针(near pointer)只包括偏移地址,不包含段地址。
  • les dest, src ,远指针举例
假设当前段地址存在如下变量:
2000:0000 12h
2000:0001 23h
2000:0002 34h
2000:0003 45h
假设bs=2000h, bx=0
les di, dword ptr ds:[bx]
这个指令,就会把3445h赋给es,把1223h赋值给di,再执行:
mov al, es:[di]; AL=byte ptr 3445:[1223]

扩充指令

符号扩充指令
  • cbw:convert byte to word
  • cwd:convert word to double word
  • cdq:convert double word to quadruple word
  • 举例:
 mov al, 05h  
 cbw ;将al8位扩充为16位
零扩充指令
  • 零扩充指令: movzx
movzx ax, al; zx:zero extension
movzx eax, al;
movzx ebx, cx;

字符串操作指令

字符串传送指令
  • MOVSB,MOVSW,MOVSD
rep movsb
其中rep表示repeat,s表示string,b表示byte
在执行此指令前要做以下准备工作:
ds:si源字符串(si就是source index)
es:di目标字符串(di就是destination index)
cx=移动次数
DF=0即方向标志设成正方向(用指令cld)
字符串比较指令
  • CMPSB,CMPSW,CMPSD
在执行此指令前要做以下准备工作:
ds:si源字符串(si就是source index)
es:di目标字符串(di就是destination index)
cx=移动次数
比较byte ptr ds:[si]与byte ptr es:[di]
当DF=0时,SI++,DI++
当DF=1时,SI--,DI--
DF=0即方向标志设成正方向(用指令cld)
repe cmpsb;(若本次比较相等则继续比较下一个)
repne cmpsb(若本次比较不等则继续比较下一个)

注意:
编程过程中遇到过,假设使用repe cmpsb指令,全等怎么判断,应该在该指令下面用je判断,还有就是如果找到第一个不相等的,那就一定要对si--,di--,cx++,最后才是真正的值,si和di对应的数组值时不相等的,
字符串扫描指令
  • scasb,scasw, scasd
执行scasb指令的时候
es:di目标字符串(di就是destination index)
cx=移动次数
被比较的值放在al寄存器中
cmp al, es:[di]
di++; 当DF=1时,为di--
repne scasb(在字符串找与al第一个相等的值)
repe scasb(跳过前面相等的,找第一个不相等的值)
字符串操作指令
  • stosb, stosw, stosd
stosb的操作过程如下:
es:di目标字符串(di就是destination index)
cx=移动次数
al为要赋的值
es:[di] = AL
di++; DF=1时为di--
rep stosb:循环CX次stosb
  • loadsb
lodsb的操作过程:
ds:si源字符串(si就是source index)
cx=移动次数
al为接收的寄存器
AL=DS:[SI]
SI++;当DF=1时, 为SI--

控制转移指令

  • jmp short target ; 短跳(同一段内)
短跳指令的机器码由2字节构成:
第1个字节=EB(短跳)
第2个字节=目标地址-下条指令的偏移地址
  • jmp near ptr target ; 近跳(同一段内)
近跳指令的第1个字节=E9
第2个字节=目标地址-下条指令的偏移地址
  • jmp far ptr target ; 远跳(不同段内)(这里使用远指针),jmp dword ptr 32位变量
  • 循环指令
loop  dest的操作过程:
CX = CX - 1     ; 循环次数减1
if(CX != 0)     ; 若CX不等于0,则
    goto  dest  ; 跳转至dest

call, ret指令

汇编语言中的三种参数传递方式
1、寄存器传递
2、变量传递
3、堆栈传递(从堆栈中取得参数,用[bp+寄存器或者常数]来引用输入的参数,一般[bp+4]就是第一个参数)
用堆栈传递参数有3种方式:

  • __cdecl 参数从右到左顺序压入堆栈,由调用者清理堆栈; 是C语言参数传递规范。
  • __pascal 参数从左到右顺序压入堆栈,由被调用者清理堆栈; 是Pascal语言参数传递规范。
  • __stdcall 参数从右到左顺序压入堆栈,由被调用者清理堆栈; 是Windows API函数的参数传递规范。
  • 具体讲解:C语言函数调用栈(一)      C语言函数调用栈(二)

4、C语言的函数里面除了不能破坏bp外,还要保护bx,si,di的值:

f:
	push bp
	mov bp, sp
	sub sp, n; 其中n一个常数,用来为动态变量分配空间
	push bx
	push si
	push di
	...
	pop di
	pop si
	pop bx
	mov sp, bp
	pop bp
ret

int,iret指令

     int n对应的中断向量的存储位置一定是0:n*4,n的取值范围是[00, FF], 所以256个中断向量会占用0:0~0:3FF之间共400h个字节的内存,这块区域称为中断向量表。BIOS会完成部分中断向量的填写, 如int 10h、int 16h、int 13h这几个BIOS中断的向量在DOS启动前就已经填好了; DOS启动完成后,会填入int 21h的中断向量。
    ret指令用栈中的数据,修改IP的内容,从而实现近转移
    retf指令用栈的数据,修改CS和IP的内容,从而实现远转移

int指令会完成三件事:
pushf ;保存标志寄存器
push cs ;当前代码段的段地址
push ip  ;下一条执行的指令

iret指令(中断函数用的返回指令)会完成三件事
pop ip
pop cs
popf

ret指令会完成一件事:
pop ip;下一条执行的指令
retf指令会完成两件事:
pop ip
pop cs
call指令会完成一件事:
push ip;下一条执行的指令

下面内容转载自:https://blog.csdn.net/abc_12366/article/details/79774530
当执行call指令时,进行两步操作:
1)将当前的IP或CS和IP压入栈中
2)转移
call指令不能实现短转移,它的书写格式同jmp指令

依据标号进行转移的call指令
语法格式:call 标号
汇编解释:(1) push IP (2) jmp near ptr 标号

依据目的地址在指令中的call指令
语法格式:call far ptr 标号
汇编解释:(1) push CS (2) push IP (3) jmp far ptr 标号

转移地址在寄存器中的call指令
语法格式:call 16位reg
汇编解释:(1) push IP (2) jmp 16位reg

转移地址在内存中的call指令
语法格式一:call word ptr 内存单元地址
汇编解释一:(1) push IP (2) jmp word ptr 内存单元地址

语法格式二:call dword ptr 内存单元地址
汇编解释二:(1) push CS (2) push IP (3) jmp dword ptr 内存单元地址

实践

键盘输入两个十进制非符号数(≤65535),计算两数之乘积,
分别以十进制、十六进制、二进制输出结果。

.386
data segment use16
s db 20, ?, 20 dup(0)
t db 20, ?, 20 dup(0)
res db 64 dup(0)
data ends
code segment use16
assume cs:code, ds:data

;输出换行
huanhang:
    mov dl, 0Dh
    mov ah, 02h
    int 21h
    mov dl, 0Ah
    mov ah, 02h
    int 21h
    ret

;将输入数字转为数存到寄存器中
transform:
    ;将十进制字符串转为16位整数
    mov si, 0 ;数组s的下标
    mov ax, 0 ;被乘数=0
again:
    mov cx, 0
    mov cl, ds:[bx+si]
    cmp cl, 0Dh
    je trans_done
    mov di, 10
    mul di
    sub cl, '0'
    add ax, cx
    inc si
    jmp again
trans_done:
    mov byte ptr ds:[bx+si], '$'
    ret

;将计算结果转为二进制
binary:
    mov si, 0
    mov bx, 32
binary_loop:
    mov cl, 1
    rol eax, cl
    push eax
    and al, 01h
    add al, '0'
    mov dl, al
    mov ah, 02h
    int 21h
    inc si
    mov di, si
    and di, 03h
    cmp di, 0
    jne binary_last
    cmp si, 32
    je binary_last
    mov dl, ' '
    mov ah, 02h
    int 21h
binary_last:
    pop eax
    dec bx
    jnz binary_loop
    ret

;将计算结果转为十进制
decimal:
    mov si, 0
    xor edx, edx
    mov ebx, 0Ah
decimal_loop:
    xor edx, edx
    div ebx
    cmp eax, 0
    je decimal_done
    add dl, '0'
    mov res[si], dl
    inc si
    jmp decimal_loop
decimal_done:  
    add dl, '0'
    mov res[si], dl
    ret

;将计算结果转为十六进制
hex:
    mov bx, 8
hex_loop:   
    mov cl, 4
    rol eax, cl
    push eax
    and al, 0Fh
    cmp al, 10
    jb is_digit
is_aplha:
    sub al, 10
    add al, 'A'
    jmp hex_done
is_digit:
    add al, '0'
hex_done:
    mov dl, al
    mov ah, 02h
    int 21h
    pop eax 
    dec bx
    jnz hex_loop
    ret

main:
    mov ax, data
    mov ds, ax
    lea dx, s
    mov ah, 0Ah
    int 21h
    mov dl, 0Ah
    mov ah, 02h
    int 21h
    lea dx, t
    mov ah, 0Ah
    int 21h
    ;存放偏移地址
    lea bx, s[2]
    call transform
    push ax
    lea bx, t[2]
    call transform
    push ax
    pop bx
    pop ax
    mul bx
    
    ;存储计算结果
    push ax
    push dx

    mov dl, 0Ah
    mov ah, 02h
    int 21h
    
    ;输出
    lea dx, s[2]
    mov ah, 09h
    int 21h
    mov dl, '*'
    mov ah, 02h
    int 21h
    lea dx, t[2]
    mov ah, 09h
    int 21h
    mov dl, '='
    mov ah, 02h
    int 21h
    call huanhang
    
    pop ax
    shl eax, 16
    pop ax
    push eax
    call hex
    
    mov dl, 'h'
    mov ah, 02h
    int 21h
    call huanhang
    
    pop eax
    push eax
    call binary
    
    mov dl, 'B'
    mov ah, 02h
    int 21h
    call huanhang

    pop eax
    call decimal
    lea bx, res
output:
    mov dl, res[si]
    mov ah, 02h
    int 21h
    dec si
    cmp si, 0
    jge output ;这里一定要用符号数的比大小
done:
    mov ah, 4Ch
    int 21h

code ends
end main
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值