汇编语言学习笔记
https://www.cnblogs.com/secondtonone1/p/6617884.html
汇编语言学习笔记(一)
一:变量类型
汇编语言变量基本类型如下:
sdword :表示32位整数
dword:表示32位无符号整数
sword:表示16位整数
word:表示16位无符号整数
sbyte:表示8位整数
byte:用于表示字节,大小为8位
变量的表示和定义:
C语言中
int num3 = 5;
汇编中
num3 sdword 5;
C语言中
char name1;
char name2=‘A’;
汇编中:
name1 byte ?
name2 byte ‘A’;
C语言中:
char name[] = "iloveu";
汇编中:
name byte ‘iloveu’,0 //字符串结汇通常带有二进制的0,占用一个字节。
二:立即数的存储
在汇编语言中将一个常量存储在内存空间需要通过mov操作,立即数为一个常量。
将5存储在变量num1中
num1 sdword ?;
mov num1 5;
mov操作后面两个参数为内存或寄存器,mov两个参数可以为以下关系:
mov mem , imm 将立即数存储到内存中
mov reg, mem 将内存单元的数据存储到寄存器中
mov mem, reg 将寄存器中的内容存储到内存单元中
mov reg, imm 将立即数存储在寄存器单元中
mov reg, reg 将第二个寄存器中的内容存储到第一个寄存器中
但是不能mov mem, mem, 不能将一个内存中的内容存储到另一个内存中
需要通过mov reg, mem1; mov mem2 reg;可达到目的。
三:寄存器
数据不能直接从一个内存单元中移动到另一个内存单元中,需通过寄存器做中转。
内存单元数据放在随机存储器(RAM)中,RAM将内存单元中的数据拷贝到CPU的寄存器中,
然后将CPU的寄存器中的数据copy到另一个存储单元中。
将内存单元的内容复制到寄存器中的动作成为装载,将cpu中寄存器的数据copy到内存中成为存储。
不是所有的寄存器都可以被开发者使用,提供给程序员的寄存器为通用寄存器。
16位通用寄存器为ax,bx,cx,dx
32位通用寄存器为eax,ebx,ecx,edx
以eax举例,eax低16位为ax寄存器,ax寄存器低8位为al寄存器,高8位为ah寄存器。如图所示:
eax为累加器,主要用于参与算术逻辑
ebx为基址寄存器,用于处理数组
ecx为计数器,用于循环操作
edx为数据寄存器,用于算术指令。
除此之外还有eflags表示状态的寄存器,esp堆栈指针指向栈顶部,ebp基址指针指向栈底部。
四:输入输出
字符串的输出
C语言版:
#include<stdio.h> int main() { printf("%s\n"," Hello World!"); return 0; }
用汇编实现为:
.386 .model flat ,c .stack 100h printf PROTO arg1:Ptr Byte, printlist:VARARG .data msg1fmt byte "%s", 0Ah, 0 msg1 byte "Hello World", 0 .code main proc INVOKE printf, ADDR msg1fmt, ADDR msg1 ret main endp end
.386是一个汇编指令,它指明程序可以被汇编为在Intel386系列或更高级的计算机运行,
.model flat表明程序使用保护模式,.model flat ,c表示该程序可以与C或C++程序进行连接,且需要运行在Visual C++环境中。
.stack 100h 表示堆栈的大小应该达到十六进制100个字节
printf 名称后面的 PROTO表示printf为函数原型, arg1表示printf第一个参数为指向Byte类型的Ptr指针。
第二个参数为VARARG类型可变参数printlist
.data表示数据段,.code表示代码段。
msg1fmt 为byte类型的字符串,0Ah为16进制表示\n,0表示字符串结尾\0
msg1为byte类型的字符串,“Hello World”
proc汇编指令表示一个过程,过程名称为main
INVOKE 为汇编指令,调用函数printf完成输出,
格式为 INVOKE 函数名, 参数一,参数二
这里为msg1fmt字符串的首地址,以及msg1的首地址。
ADDR为取地址操作。
ret表示返回和C语言return 0一样。
main endp表示main proc结束
end表示整个汇编程序结束。
整数的输出和字符串类似,下面通过输入输出程序介绍汇编语言的输入和输出
.386 .model flat c .stack 100h printf PROTO arg1:Ptr Byte, printlist:VARARG scanf PROTO arg2:Ptr Byte, inputlist:VARARG .data in1fmt byte "%d", 0 msg0fmt byte 0Ah, "%s", 0 msg1fmt byte 0Ah, "%s%d", 0Ah, 0Ah, 0 msg0 byte "Enter an integer for num1: ", 0 msg1 byte "The integer in num2 is: ",0 num1 sdword ? num2 sdword ? .code main proc INVOKE printf, ADDR msg0fmt, ADDR msg0 INVOKE scanf, ADDR in1fmt, ADDR num1 mov eax, num1 mov num2, eax INVOKE printf, ADDR msg1fmt, ADDR msg1, num2 ret main endp end
输入和输出类似,C语言中输入函数为scanf
汇编程序通过
scanf PROTO arg2:Ptr Byte, inputlist:VARARG PROTO表示scanf为函数原型, 第一个参数为指向Byte的指针 第二个参数为可变参数 scanf输入整数num1,需要取地址,所以会采用ADDR num1 将上述汇编转译为C语言
#include<stdio.h> int main() { int num1, num2; printf("\n%s", "Enter an integer for num1: "); scanf("%d", &num1); num2 = num1; printf("\n%s%d\n\n", "The integer in num2 is: ", num2); return 0; }
五:算数指令
加法指令
add mem, imm 将立即数imm和内存单元mem中数值相加存储在mem中
add reg, mem 将内存mem中的数值和寄存器reg中的数值相加,结果存储在reg中
add reg, imm 将立即数imm和寄存器reg中数据相加存储在reg中
add mem, reg 将寄存器中的数据和内存单元mem中数据相加存储在mem中
add reg, reg 将寄存器中的数据和寄存器相加,最后存储在第一个reg中
注意:不可add mem, mem 也不可 add imm, imm 两个立即数没法相加结果也没办法存储
两个内存单元也没办法相加,和mov mem, mem是一样的,都不可以直接操纵两个内存。
减法指令
sub mem, imm 将内存单元mem中数据减去立即数imm存储在mem中
sub reg, mem 将寄存器reg中数据减去mem中的数据,存储在reg中。
sub mem, reg 将内存单元mem中数据减去寄存器reg中数据,结果存储在mem中
sub reg ,imm 将寄存器reg中的数据减去立即数imm,将结果存储在reg中
sub reg, reg 将第一个reg数据减去第二个reg数据,最后结果存储在第一个reg中
注意:不可sub mem, mem 也不可 add imm, imm 两个立即数没法相减结果也没办法存储
两个内存单元也没办法相减,和mov mem, mem是一样的,都不可以直接操纵两个内存。
乘法指令imul
imul mem 将内存单元mem中的数据和寄存器eax中的数据相乘,结果存储在edx:eax中
imul reg 将寄存器reg中的数据和寄存器eax中的数据相乘,结果存储在edx:eax中
注意:imul 只有一个参数,这个参数只能为内存单元或者寄存器,不能是imm立即数
由于乘法指令imul可能会造成乘积位数大于乘数和被乘数的位数,所以将结果存储在edx:eax中
imul为有符号的乘法指令, mul为无符号的乘法指令, imul和mul参数只能为mem或reg,如果
想计算num = mem* 3;mem为内存单元数据,3为立即数
imul实现为
mov eax, 3
imul mem
mov num, eax
除法指令idiv
和乘法指令类似,idiv 为有符号除法, div为无符号除法
进行除法操作前将被除数放入edx:eax中,除法操作后edx存储的为余数,
eax存储的为商。举例 num = 13/,5;
汇编语言实现过程先将13放入eax中,
调用cdq 可以将eax内容扩展到edx:eax寄存器对中,将5放入内存单元或寄存器中,
调用idiv最终edx存储为3,eax存储为2
汇编语言实现如下:
mov eax, 13 mov ebx,5 cdq idiv ebx mov num , eax
cbw 指令可将字节转换为字,一个字为两个字节,即将al内容扩展到寄存器ax中
cwd 字转换为双字, 即将ax中的内容扩展到eax中
cdq 双字转换为4个字, 即将eax中内容扩充为edx:eax中。
只有cdq允许有符号位,所以进行idiv和div操作前执行cdq将eax中内容扩充到
edx:eax中。
注意:idiv和div 只有一个参数,这个参数只能为内存单元或者寄存器,不能是imm立即数
自增自减指令
dec reg reg中的数据自减
dec mem mem中的数据自减
inc reg reg中数据自增
inc mem mem中数据自增。
注意:dec 和inc 只有一个参数,这个参数只能为内存单元或者寄存器,不能是imm立即数
取反指令,也就是求一个数的补码
neg mem
neg imm
注意:neg 只有一个参数,这个参数只能为内存单元或者寄存器,不能是imm立即数
下面两个例子是综合计算
; v = -w + m * 3 - y/(z-4) ;w,v,m,y, z分别为内存单元 mov ebx, w neg ebx mov eax,3 imul m add eax, ebx mov v ,eax mov ebx, z sub ebx, 4 mov eax, y cdq idiv ebx sub v, eax
汇编语言基础第一部分记录到此为止,下一篇跟进记录
重剑无锋,大巧不工
分类: 汇编学习
https://www.cnblogs.com/secondtonone1/p/6652155.html
汇编语言学习笔记(二)
六、选择结构
if-then结构
C语言版本
if(count == 10) { count --; i++; }
MASM汇编
.if count==10 dec count inc i .endif
cmp指令,该指令用于比较两个参数大小
cmp mem, imm 比较内存mem和立即数imm大小
cmp reg, imm 比较寄存器reg和立即数imm大小
cmp reg, mem 比较寄存器reg和内存mem大小
cmp mem, reg 比较内存mem和寄存器reg大小
cmp imm, reg 比较立即数和寄存器reg大小
cmp imm, mem比较立即数和内存mem大小
cmp reg, reg比较两个寄存器数据大小
.if 内部是通过cmp实现的,.if和cmp都不能比较两个内存中数据大小
条件跳转指令
je 相等时跳转
jne 不相等时跳转
je 和jne可以用于无符号数比较跳转,也可用于符号数比较跳转
符号数据跳转指令
jg 大于时跳转
jge 大于或等于时跳转
jl 小于是跳转
jle 小于或等于时跳转
jnle 不小于且不等于时跳转,即大于时跳转
jnl 不小于时跳转
jnge 不大于且不等于时跳转
jng 不大于时跳转
这类指令可用上面的jg,jge,jl,jle替代,尽量不要使用,
不容易理解。
.if-.end汇编指令可用上面的cmp指令和条件跳转指令替代
如
.if number == 0 dec number .endif
用条件跳转指令实现为:
if01: cmp number, 0 jne endif01 then01: dec number endif01: nop
if-then-else结构
汇编指令的结构为
.if 条件 ;执行语句 .else ;执行语句 .endif
举例说明,C语言
if (x>=y) x--; else y--;
汇编语言
if01: mov eax,x cmp eax,y jl else01 then03: dec x jmp endif01 else01: dec y endif01: nop
采用汇编指令
mov eax , x .if eax >= y dec x .else dec y .endif
嵌套if结构
C语言
if(x < 50) y++; else if(x <= 100) y = 0; else y--;
汇编语言
采用汇编指令
.if x < 50 inc y .elseif x <= 100 mov y, 0 .else dec y .endif
通过跳转指令实现上述步骤
if01: cmp eax, 50 jge else01 then01: inc y jmp endif01 else01: nop if02: cmp x, 100 jg else02 then02: mov y,0 jmp endif01 else02: dec y endif01: nop
case 结构
C语言
switch(w) { case 1: x++; break case 2: case 3: y++; break; default: z++; }
汇编语言中没有case switch对应的汇编指令,但是可以通过cmp指令和跳转指令实现
switch01: cmp w, 1 je case1 cmp w,2 je case2 cmp w,3 je case2 jmp default01 case1: inc x jmp endswitch01 case2: inc y jmp endswitch01 default01 : inc z endswitch01: nop
无符号跳转指令
ja 高于跳转
jae 高于或等于跳转
jb 低于跳转
i = 1; while(i <= 3) { i++; }
jbe 低于或等于时跳转
逻辑运算符综合实现
.if w==1 || x==2&&y == 3 inc z .endif
条件跳转指令实现上述逻辑
if01: cmp w,1 jne or01 or01: cmp x, 2 jne endif01 cmp y, 3 jne endif01 then01: inc z endif01: nop
七、迭代结构
1 前置循环结构while
C语言
i = 1; while(i <= 3) { i++; }
汇编指令实现
mov i, 1 .while (i <= 3) inc i .endw
条件跳转指令实现
mov i, 1 while01: cmp i, 3 jg endw01 inc i endw01: nop
2 后置循环结构 do while
C语言
i = 1; do { i++; }while(i <= 3);
汇编指令 .repeat .until
mov i, 1 .repeat inc i .until i > 3
.repeat-.until和C语言do -while一样,第一次先执行循环体内容,do-while是在每次运行循环体后判断条件不满足跳出循环,
.repeat-.until 是判断条件不满足则一直循环,直到.until后面条件成立则跳出循环。
通过汇编跳转指令实现
mov i, 1 repeat01: nop inc i cmp i, 3 jle repeat01 endrpt01: nop
3 固定循环结构 for
C语言
for(i=1; i <= 3; i++) { //循环体 }
汇编语言
mov ecx, 3 .repeat ; 循环体 .untilcxz
.repeat-.untilcxz 对应C语言的for循环固定循环结构,它和.repeat-.until都是后置循环结构。
都要先执行一次循环体,然后.untilcxz会将ecx中的数据值减1,接着判断ecx是否为0,ecx为0则跳出循环
ecx大于0则继续执行循环。所以执行.repeat-.untilcxz这个循环前保证ecx大于0
.repeat-.untilcxz 内部是loop实现的,所以同样可以通过loop实现该循环
mov ecx, 3 for01 : nop ; 循环体 loop for01 endfor01: nop
loop 和.untilcxz一样,都会将ecx中的值减1,然后判断ecx是否为0,为0则终止循环,大于0则继续循环。
loop指令最多只能向前跳转128字节处,也就是循环体内容不得超过128字节。同样.repeat-.untilcxz循环体也不得超过128字节。
与.if高级汇编指令一样,.while-end高级汇编指令和.repeat-.until高级汇编指令都基于cmp,不可直接比较两个内存变量。
使用loop指令和.repeat-untilcxz指令前,需要保证ecx大于0,在循环体中尽量不要更改ecx数值,如果需要更改则将ecx
数据先保存在内存中,执行完相关操作后将ecx数值还原,保证.untilcxz和loop操作ecx的准确性。
下面用汇编语言实现斐波那契数列
if n= 0, then 0
if n = 1, then 1
if n = 2, then 0+ 1 = 1
if n = 3, then 1+ 1 = 2
if n = 4, then 1+2 = 3
n 为任意非负数,计算n对应的数值sum。
用.while循环实现如下
.if n==0
mov sum, 0
.endif
.if n==1
mov sum, 1
.endif
mov ecx, n
mov eax, 0
mov sum, 1
.while (ecx <= n)
mov ebx, sum
add sum, eax
mov eax, ebx
.endwhile
到目前为止,循环和条件判断就介绍到这里,下一期介绍堆栈位移之类的指令。
https://www.cnblogs.com/secondtonone1/p/6664598.html
汇编语言学习笔记(三)
八、逻辑运算指令
数字在计算机中以二进制存储,每个位数字为0或者1,当两个二进制数字进行逻辑按位&,逻辑按位|,逻辑异或^操作,
可以采用汇编语言提供的逻辑运算指令and,or, xor,not等指令。
and指令:
C语言&操作,将0110&1101得出结果为0100
C语言代码为
if(flag & maskit) count++;
汇编语言高级指令可实现:
mov eax, flag .if eax & maskit inc count .endif
不采用高级指令的情况下可采用如下代码:
if01: mov eax, flag and eax, flag jz endif01 then01: inc count endif01: nop
and指令和cmp指令一样,and运算执行后,eflags寄存器的响应比特位会被设置。
同样xor,not, or等指令也会设置eflags相应比特位。
jz 指令之前介绍过,当eflags寄存器相应比特位被设置为0后该指令将逻辑跳转到指定位置。
同样ZERO?也可以判断eflags寄存器相应比特位是否被设置为0,为零则为真,反之为假
mov eax, flag and eax, maskit .if !ZERO? inc count .endif
or指令:
or指令和and指令用法一样
;flag = flag | maskit mov eax, flag or eax, maskit mov flag, eax
xor指令:
;flag = flag ^ maskit; mov eax, flag xor eax, maskit mov flag, eax
三种指令的两个参数类型限制是一样的
and mem, imm
and mem, reg
and reg, reg
and reg, imm
and reg, mem
or, xor参数类型和上面and一样,三种命令都不可以直接操作两个内存变量。
九、逻辑移位指令
shl reg, cl 将reg中数据向左移动cl寄存器中数值大小的位数。
shl reg, imm 将reg中数据向左移动imm立即数大小的位数。
举例:
mov cl , 3 shl reg, cl ;也可以采取立即数 shl reg, 3
上述代码都是将reg中数值向左移动三位,那为什么采用cl寄存器呢?因为在老式
的8086/8088处理器上,操作数中能使用的唯一立即数为1,如果调用shl reg,3
会出问题,所以通常的做法是mov cl, 3先将3移动到cl寄存器中,再调用
shl reg, cl可完成reg数值向左移动3位。
shl 的参数出去reg,还可以是mem内存
shl mem, cl
shl mem, imm
同样shr为向右移动,参数形式和shl一样。
如果10101010调用shl向左移动一位,那么所有比特位的数字依次左移,
第0个位置比特位数字填充0,第7个比特位数字1向左移动到CF进位标记里。
数字变为01010100。
同理,shr逻辑右移也是移动后第7个比特位补充0,第0个比特位数字放入CF标记中。
写一段代码测试每个比特位数字为1的位数。
mov count, 0 mov ecx, 8 mov temp , al .repeat mov ah, al and ah, 00000001b .if !ZERO? inc count .endif shr al, 1 .untilcxz mov al, temp
由于and指令会导致第一个参数数值被修改,所以将al数值每次and操作前都
copy到ah中。最后处理完所有比特位,将temp数据copy回al中。
test命令可以避免and修改第一个参数数值的问题,同样test命令使用后
eflags寄存器相应的比特位也会被设置。
mov count, 0 mov ecx, 8 mov temp, al .repeat test al, 00000001b .if !ZERO? inc count .endif shr al, 1 .untilcxz mov al, temp
十、算术移位指令
算数左移
sal reg, cl
sal reg, imm
sal, mem, cl
sal reg, cl
参数和shl一样。移动效果和shl一样,都是将低比特位依次移动到高比特位,最高比特位
移动到CF标记里,空位补0。
移动示意图:
如果符号位为0,那么左移操作和shl一样,没什么问题。
如果符号位也就是第七个比特位为1,如1111 0000,向左移动一位那么数据为
1110 0000,左移操作后数值为原数值乘以2,负数在计算机中以补码形式存储。
整数的补码为原码,负数的补码为整数原码按位取反末尾加1, 1111 0000-1为
1110 1111,按位取反后为0001 0000,该数值为十进制16,所以1111 0000为
-16,-16乘以2,也就是左移操作,为-32,那么-32在二进制中如何表示呢?
32用8位二进制表示为0010 0000,负数为正数原码按位取反末尾+1,0010 0000取反后
为1101 1111,末尾+1为1110 0000, 1110 0000恰好为1111 0000左移一位得到的数字。
考虑这样一个问题,如果是图中所示数字10101010左移会造成数字变为0101 0100,这样
最高位变为0怎么办?
这就是所谓的左移溢出,8位二进制能表示的二进制数为0111 11111~1111 1111 。
0111 1111 表示十进制127
1111 1111 表示十进制-1
1000 0000 是负数补码, 该数-1取反后得到1000 0000 该数值为128原码
所以1000 0000 表示-128,
那么8位二进制能表示的十进制数为127~-128
10101010表示的负数为-86,超过-64了。
凡是超过-64都会引起左移位溢出,负数补码出去符号位,最高位为0,此时逻辑左移会导致数据溢出。
算数右移
算数右移保持符号位不变,其余比特位依次向右移动,空出的比特位补零,最右边的比特位移除后进入CF标记。
计算product = num * 8;可通过移位指令完成,速度更快
;product = num1 * 8; mov eax, num1 sal eax, 3 mov product , eax
乘以8,不是sal eax, 8,这样是乘以256,应该为移动三位。
answer = amount /4 ;
;answer = amount/4; mov eax, amount shr eax, 2 mov answer, eax
十一、循环移位指令
循环移位将末尾移除来的数据放入另一端空缺的比特位。
循环左移位
rol reg, cl
rol reg, imm
rol mem, cl
rol mem, imm
循环右移位
ror reg, cl
ror reg, imm
ror mem, cl
ror mem, imm
循环右移位
循环左移位
之前检测一个8位二进制数中比特位数字为1的比特位个数程序可通过循环右移完成,
因为循环右移8位后,移位的结果和初始数值一样。
mov count , 0 mov ecx, 8 .repeat test al, 0000 0001b .if !ZERO? inc count .endif ror al, 1 .untilcxz
十一、堆栈操作
push 指令
push reg
push mem
push imm
push指令为入栈指令,参数可以为寄存器reg, 内存mem, 立即数imm。
pop指令
pop mem
pop reg
pop出栈指令,参数只能为内存和寄存器。
将监测比特位数值为1的比特位数程序和栈操作指令结合
push eax push al mov count, 0 mov ecx, 8 .repeat test al, 0000 0001b .if !ZERO? inc count .endif shr al,1 .untilcxz pop al pop eax
十二、xchg交换指令
xchg交换指令用于交换两个地址空间数据,xchg参数可以为以下几种
xchg reg, reg
xchg reg, mem
xchg mem, reg
同样不可以操作两个内存。
实现两个数交换
temp = num1; num1 = num2; num2 = temp;
用mov指令实现为:
mov edx, num1 mov eax, num2 mov num1, eax mov num2, edx
用push, pop指令实现为
push num1 push num2 pop num1 pop num2
用xchg指令实现为
mov eax, num1 xchg eax, num2 mov num1, eax
三种办法的效率比较为mov指令最快,其次xchg指令,最后为push pop指令。
这一篇就介绍这里,下一篇讲述宏和过程。
https://www.cnblogs.com/secondtonone1/p/6679330.html
汇编语言学习笔记(四)
十三 过程
汇编语言的过程可以被理解为方法,过程调用采取如下形式
call pname
pname为过程名,call为调用指令
pname过程的格式为
pname proc ; 过程体 ret pname endp
proc 告知编译程序过程pname的开始,endp告诉编译程序过程pname的结束。ret指令表明何时返回到调用程序中,
ret和高级语言return不一样,ret不返回任何值给调用程序。ret是过程中必不可少的调用指令。
举个过程的实例:
sample proc .if eax == 0 mov eax, 1 .else mov edx, 0 .endif ret sample endp
如果过程中使用到ecx,eax,ebx, edx等通用寄存器,而不确定外界是否也用到这些通用寄存器,
那么在过程中需要将外界用到的寄存器数据保存,可以使用临时变量,也可以使用入栈。
譬如ecx数据在调用程序中使用,同时过程mult中也用到ecx,那么使用ecx之前将ecx入栈。
mult proc push ecx mov eax, 0 .if x != 0 mov ecx, 1 .while ecx <= y add eax, x inc ecx .endw .endif pop ecx ret mult endp
出了过程中明显能从代码中看到一些寄存器会被修改,还有一些指令会导致寄存器数据被修改。
INVOKE指令会导致eax,ecx,edx内容被修改
imul会导致eax,edx被修改。所以可以通过pushad和popad指令保存和恢复这些寄存器的内容
举例
blankln proc pushad .repeat INVOKE printf, ADDR blnkfmt dec ebx .until ebx <= 0 popad ret blacnkln endp
过程的声明都是放在main主程序之后。而且过程更看重的是节约空间。
十三 宏
1 宏和过程都可以避免编写同样功能代码,宏的速度比过程快,但是倾向于浪费一些空间达到提高效率的目的。
宏的声明放在main函数之前。
宏的结构如下:
mname macro ; 宏体 endm
宏的结构和过程的结构不同,宏内部没有ret,并且macro 表示宏的开始 macro前边为宏的名字。
endm为宏的结束,但是endm之前没有宏的名字。另外宏的调用不需要使用call指令,直接写宏的名字即可。
可以通过查看汇编列表文件看看宏扩展是什么,以及宏被插入到程序段是什么指令,下面为某段代码调用两次swap宏
展开后:
88 1D 00000046 表示指令mov ebx , num1,该指令在程序地址为00000000处。
另外需要注意的是宏扩展后,注释也会扩展在指令后边。虽然会占用一定内存,但是注释可以帮助程序人员排除逻辑错误。
如果想使用注释,仅仅在宏过程中可见那么采取;;替代;,这样注释不会出现在宏扩展中,用户可以通过宏的声明看到注释。
调用宏的时候,调用几次宏就在相应的调用位置扩展几次,而过程无论调用多少次,仅仅扩展一次。这就是过程节省空间的
原因。
另外宏内部尽量不要使用寄存器,如果使用寄存器也不需要保存和恢复寄存器内容,这些操作放在调用宏的程序里,因为保存和恢复
操作一则浪费空间,二则会减少宏执行的效率。
2 带参数的宏
swap macro p1, p2 mov ebx, p1 xchg ebx, p2 mov p1, ebx endm
p1,p2为宏的参数,可以理解为高级语言宏的两个参数,调用程序调用swap时会将实参替代形参完成宏调用。
调用swap时会用num1替代p1,num2替代p2,
用x替代p1, y替代p2
如果开发人员只传入一个参数怎么办?
比如 swap num1?
这样宏展开就出现问题。可以通过:REQ语句指明参数是必须传入不可缺少的。
swap macro p1:REQ, p2:REQ mov ebx,p1 xchg ebx, p2 mov p1, ebx endm
这样要求调用swap必须传入两个参数,缺少参数则提示报错。
考虑这样一个问题,如果调用程序调用swap num1,1怎么办?
xchg ebx,1出错。
另外如果 swap ebx, ebx 怎么办?这种没必要调用swap,虽然调用swap不会出错,
但是造成了空间的浪费。这些问题都可以通过条件汇编解决。
十四 条件汇编
条件汇编和条件指令不同,条件汇编控制编译程序,而不是控制程序的执行,
条件汇编根据不同的条件将不同的指令或代码包含到程序中。
条件汇编指令if
if和之前介绍的高级汇编指令.if不一样,if后边的参数为 eq, ne, lt, le, gt, ge, or, and等。
举例:
if num eq 3
;该条件满足将指令插入程序
endif
除此之外
ifb 判断如果为空,则执行if下边的逻辑。
ifnb 判断如果不为空
ifidn 判断如果相同
ifidni 不区分大小写,如果相同
ifdif 如果不相同
ifdifi 不区分大小写,如果不同
这类指令后边要用<参数形式>
举例:
addacc macro parm ifb <parm> inc eax else add eax, parm endif endm
在程序中通过几种方式调用addacc
.lst文件内容如下所示,根据不同条件扩展为不同的宏
根据不同的条件生成了不同的机器指令,达到了节约空间和控制编译的目的。
仅仅在一部分地址和机器指令行包含了汇编指令,其余没有机器指令和地址的汇编代码不会
被包含在程序中。
使用条件汇编将之前的swap设计的更完善
swap macro p1:REQ, p2:REQ ifidni <ebx>,<p2> xchg p1, ebx elseifidni <p1>, <ebx> xchg ebx, p2 else mov ebx, p1 xchg ebx, p2 mov p1, ebx endif endm
通过条件汇编,使swap功能更健全和高效。
十五 过程和宏对比和总结
1 过程在被调用的时候只有一份程序副本出现,而宏在被调用的时候,每一次对宏的调用都会出现一次宏代码的副本。
2 过程通常会保存和恢复寄存器的内容,而宏通常不会去保存和恢复寄存器的内容。
3 过程倾向于节省内存空间,而宏倾向于节省运行时间
4 调用过程的时候,使用call指令,后面跟着过程的名字,而调用宏的时候直接写宏的名字。
5 过程中必须包含ret指令,但是宏中一定不能写ret
6 把过程名字放到endp语句之前的标记字段,但是endm之前的标记字段不需要写宏的名字。
7 如果要求调用宏必须传入参数,可在参数后加:REQ
8 条件编译if在使用or或and逻辑时需要将两个参数用括号括起来,如if(x lt 0)or(y gt 1)
https://www.cnblogs.com/secondtonone1/p/6700449.html
汇编语言学习笔记(五)
十六、数组
数组的基本表示方法
numary sdword 2,5,7
numary数组中有三个元素,为sdword类型,分别为2,5,7
empary sdword ?, ?,?
empary数组为sdword类型元素,未初始化。
如果数组元素很多可通过
zeroary sdword 100 dup(0)
zeroary数组中有100个0
empary sdword 3 dup(?)
empary 数组中有3个未初始化的sdword类型数据
mov eax, numary+8; 表示把数组numary第3个元素放入eax中
sdword为四字节,numary+0表示numary首地址,也是第一个元素地址,以此类推,numary+8为第三个元素首地址。
mov numary+0, eax; 将eax内容放入数组第一个元素中。
出了采用数组首地址+偏移地址方式,可以采用ebx基址寄存器进行数组索引。
访问numary第三个元素
mov ebx, 8;ebx存储8
mov eax, numary[ebx];访问numary偏移8字节的元素,也就是第三个元素,放入eax中。
举个例子C语言:
sum = 0 for(i = 0; i < 3; i++) { sum +=numary[i]; }
汇编语言:
mov sum, 0 mov ecx ,3 mov ebx, 0 .repeat mov eax, numary[ebx] add sum, eax add ebx,4 .untilcxz
除了使用基址寄存器ebx,还可以使用寄存器esi和寄存器edi进行索引,
esi为源索引寄存器,edi为目的索引寄存器。
第一种方法
mov ebx,4
mov eax,numary[ebx]
第二种方法
mov esi, offset numary+4
mov eax,[esi]
第二种方法先将numary+4的地址放入esi中,
然后[esi]取出esi指向的地址空间的数据,也就是numary+4地址空间里的数据
将数据放入eax中。
两种方法的效果图:
除了上述两种方法,还有第三种方法
lea esi, memory+4
mov eax, [esi]
lea 和offset的区别:
offset 是在编译时将地址写入esi
lea是动态写入,每次运行时将地址写入esi中。
去实现如下代码:
j=n-1; for(i=0; i < n/2; i++) { temp=numary[i]; numary[i] = numary[j]; numary[j] = temp; j--; }
通过汇编实现:
mov ecx,n sar ecx,1 lea esi,numary+0 mov edi, esi mov eax,n dec eax sal eax,2 add edi, eax .repeat mov eax, [esi] xchg eax, [edi] mov [esi], eax add esi,4 sub edi,4 .untilcxz
数组还有两个指令,lengthof表示数组元素的个数
sizeof表示数组总共占用多少字节。
前面的代码可以通过这两个指令修改
mov ecx, lengthof numary sar ecx,1 lea esi, numary+0 mov edi, esi mov eax, sizeof numary sub eax,4 add edi, eax .repeat mov eax,[esi] xchg eax, [edi] mov [esi], eax add esi, 4 sub edi,4 .untilcxz
十七、数组总结
1 esi 为源索引寄存器,主要用于从esi指向地址空间取出数据
2 edi为目的索引寄存器,主要用于向edi指向地址空间写入数据
3 esi和edi存储的为地址,[esi]和[edi]为他们指向的地址空间存储的数据
4 可以通过mov edi, esi将esi寄存器存储的地址放入edi中,因为两个操作数都是寄存器
5 不可以使用mov [edi],[esi];因为两个操作数都为内存,这是汇编指令mov不允许的。
6 寄存器ebx为基址寄存器,可通过 数组名[ebx]取出数组首地址偏移ebx存储的字节数的元素。
7 offset操作符的mov指令,如mov eax, offset sumary+4 是将sumary首地址偏移4字节地址写入eax,
此时eax存储的是第二个元素首地址,他是静态的获取地址,而lea是动态的获取地址
8 lengthof用于计算数组元素数量,sizeof用于计算数组总共占用多少字节。
数组的介绍到此为止,下一篇是字符串的介绍
https://www.cnblogs.com/secondtonone1/p/6710407.html
汇编语言学习笔记(六)
十八、字符串处理
前文介绍过字符串的处理,字符串是byte类型 的数组,现在实现一段代码,将字符串string1数据copy到字符串string2中
代码如下
.data string1 byte "Hello World!", 0 string2 byte 12 dup(?), 0 .code mov ecx, 12 mov ebx,0 .repeat mov al, string1[ebx] mov string2[ebx], al inc ebx .untilcxz
通过ecx递减,将字符串string1每个字符一次copy给string2中,其中用到了ebx基址寄存器。
也可以通过esi和edi寄存器
.data string1 byte "Hello World!" stirng2 byte 12 dup(?), 0 .code mov ecx,12 lea esi, string1 lea edi, string2 .repeat mov al,[esi] mov [edi],al inc edi inc esi .untilcxz
这些代码可以通过字符串操作的其他指令替代,使代码更简洁
1 movsb指令
movsb是字符串移动指令,将esi指向的字符移动到edi指向的空间,并且将ecx减1,寄存器esi和edi内容增加1或者减少1
如果仅仅使用movsb指令作用不大,配合循环使用,功能很强大。cld指令表示方向标志值清0,调用movsb会使esi和edi内容增加1
std指令表示设置方向标志值,调用movsb会使esi和edi内容减少1
还是上面的需求,这次使用movsb完成目标
mov ecx,12 ; 字符串大小为12 mov esi, offset string1 ; esi指向string1起始位置 mov edi, offset string2 ; edi指向string2起始位置 cld ; cld指令调用后 ,调用movsb会使esi和edi分别加1 .repeat movsb ; 先将esi指向的空间数据拷贝到edi指向的空间 ; 然后esi和edi分别加1,并且ecx值减少1 .untilcxz
和movsb配合使用的几个前缀:
rep 重复操作,等于循环,直到ecx为0退出循环
repe 如果相等,则重复操作,如果不等,那么退出循环,或者ecx为0退出循环
repne 不相等,则重复操作,相等则退出循环,或者ecx为0退出循环
上面的代码可以使用 rep movsb更改,完成同样的目的
mov ecx, 12 lea esi, string1 lea edi, string2 cld rep movsb ;循环直到ecx为0结束
2 scasb指令
scasb指令用于在edi寄存器指定的空间中搜寻al所存储的字符。scasb指令操作流程为:将要查询的字符放入al寄存器中,
如果要在字符串string中查找匹配的字符,那么将edi存储string的首地址,调用scasb进行匹配,scasb指令调用后会将edi
存储数值加1,并且ecx值-1,为了完成字符串string中逐个字符匹配,需要在scasb前面加上repne指令,这样在不相等时继续
查找,直到查找到指定字符退出循环。此时edi指向的位置为字符串string中匹配字符位置的下一个位置。
举例实现:
.data name1 byte "Abe Lincoln" .code mov al, ' ' ; 在al寄存器中存储空格,为了在name1中查找该空格 mov ecx, lengthof name1 ;ecx存储name1字符串长度,为11 lea edi, name1 ;edi指向name1首地址 repne scasb ;匹配edi指向空间的字符和空格是否相等,不相等则edi加1 ;ecx减1,直到相等或者ecx为0退出循环
运行后ecx为7,edi执行字符L的位置
效果图如下:
3 stosb指令
stosb用于把寄存器al存储的数据放入寄存器edi所指向的空间,指令完成后edi增加1
stosb等价于:
mov [edi],al
inc edi
4 lodsb指令
lodsb用于把寄存器esi所指向的空间数据存储到al寄存器中,指令完成后esi增加1
lodsb等价于:
mov al, [esi]
inc esi
下面实现将字符串姓名倒置功能,并将名字和姓中间添加字符“ ”和“,”
如“Abe Lincoln”变为“Lincoln, Abe”
.data name1 byte "Abe Lincoln" name2 byte 12 dup (?) .code mov al, '' mov ecx, lengthof name1 lea edi, name1 repne scasb push ecx mov esi, edi lea edi, name2 rep movsb mov al, ' ' stosb mov al, ' ' stosb mov ecx, lengthof name2 pop eax sub ecx, eax dec ecx lea esi , name1 rep movsb
5 cmpsb指令
字符串比较指令,该指令和movsb差不多,比较edi和esi指向空间数据大小,并且edi和esi增加1或者减少1,
可通过cld或std设置,并且ecx减少1
比较“James” 和 “James”
代码如下:
.data name1 byte "James" name2 byte "James" .code mov ecx, lengthof name1 lea esi, name1 lea edi, name2 cld repe cmpsb
相等情况下ecx为0,edi和esi都指向字符串最后一个字符的下一个位置
如果比较的字符为“James”和“Jamie”,那么效果如下:
如果比较字符为“Marci”和“Marcy”,那么上述代码就没办法区分是否相等了,因为此时ecx也为0
如果ecx大于0,那么两个字符串肯定不相等的,如果ecx为0,那么将esi和edi都-1,得到最后一个元素,
再将esi和edi指向空间的数据比较即可。
下面将上述代码完善一下,比较两个长度相等的字符串name1和name2
mov ecx, lengthof name1 lea esi, name1 lea edi, name2 cld repe cmpsb .if ecx > 0 ;输出不相等 .else dec esi dec edi mov al, [esi] .if al != [edi] ; 输出不相等 .else ;输出相等 .endif .endif
十九、字符串总结
1 movsb 指令将寄存器esi指向的字节类型字符串的内容移动到寄存器edi指向的位置,通过cld或std设置esi和edi增加还是减少
2 cmpsb 指令对寄存器的esi指向的字符串中的一个字符和edi所指向的一个字符进行比较,通过cld或std设置esi和edi增加还是减少
3 movsb指令前边的rep前缀会让该指令重复执行,循环次数等于ecx的值,每一次循环ecx的值减1,直到为0停止。
4 cmpsb指令前面的repe前缀的作用类似于rep前缀,当ecx为0是,停止执行指令。或者当esi和edi所指向的两个空间的字节内容不同,循环也会停止。
repne当esi和edi所指向的两个空间的字节内容相同时,停止。
5 scasb指令将一个字符串进行搜索,查找字符串中是否存在寄存器al中所存储的字符。如果找到该字符,那么edi指向的地址比该字符的地址靠后一个字节。
stosb将al寄存器的内容存储在edi所指向的字符串的位置中。lodsb指令将把寄存器esi所指向的字符串的位置中的字符复制到al寄存器中。
我的微信公众号,谢谢关注:
https://www.cnblogs.com/secondtonone1/p/6729440.html
汇编基础最后一篇--机器语言指令
这是汇编语言基础最后一篇,以后还会更新更高级的汇编知识,并且这部分知识会应用到
逆向编程的环节,这一章介绍汇编基础--机器指令。
一个16比特位的汇编指令:
opcode操作码占用3个比特位,可以表示2的3次方为8种操作
寄存器占用2个比特位,可表示2的2次方为4种可用寄存器
地址空间为2的11次方为2048个可能的内存单元地址可用。
inc 指令和dec指令
从图中可以看出这些指令长度仅为一字节,因为每个指令地址相差为1字节。
inc eax 这条指令的机器码为40, 40为16进制表示,转为二进制为 0100 0000
inc ecx 这条指令机器码为41,41位16进制表示,转换为二进制为0100 0001
依次展开会发现机器码的规律,开头都为01000XXX
而XXX实际就是寄存器在机器中表示的二进制机器码。
下面是各个寄存器的机器码:
下面看下dec 寄存器指令图:
dec 寄存器指令的格式可以总结为
01001xxx, xxx为寄存器二进制指令格式。
mov 指令
mov eax, reg指令图
每个指令长度为2字节,从地址偏移可以看出。
将上面指令机器码转为二进制
mov ecx, eax 十六进制为8B C8,二进制表示第二个字节 1100 1000
mov ecx, ecx 十六进制为 8B C9,二进制表示第二个字节为 1100 1001
通过对比二进制,发现第一个字节都为8B,第二个字节分别为 C0,C1,C2,C3...C9
mov reg, reg 指令格式为
10001011 11XXXYYY
XXX为目的寄存器, YYY为源寄存器
mov reg, imm 即将一个立即数移动到寄存器中指令的机器码会是什么样呢?
下图为将立即数移动到寄存器的图示:
mov eax, 1 指令机器码为 B8 00000001
mov ecx, 10 指令机器码为 B9 0000000A
两条指令地址相差5个字节,每个字节8bit,可计算出每条指令为40bit长度。
即10个十六进制数表示。而 B8 00000001 和 B9 0000000A 恰巧为10个16进制数字组成。
机器码B8 00000001 从左向右数,去掉B8占用的一个字节,剩下的四个字节可以看出用来表示
立即数 1。同样的道理,可以看出 B9 0000000A也是这个原理。
如果移动的为负数,怎么表示呢?
mov edx, -1 这个指令我们分析一下 -1在机器中的表现形式
负数在机器中以补码的形式表现,-1 的补码计算规则为:
1的源码为 0000 0000 0000 0000 0000 0000 0000 0001
按位取反 为 1111 1111 1111 1111 1111 1111 1111 1110
末尾+1 位 1111 1111 1111 1111 1111 1111 1111 1111
转换为十六进制为 F F F F F F F F 恰好就是 机器码的最后三个字节表示。
同样的道理适用于mov ebx, -10
下面分析前两个字节 BA, BB, B8, B9 分别有什么关联。
同步对比可以看出 前几个比特位是一样的,都为 10111
后三个比特位分别为 000, 001, 010, 011,这四个二进制码恰好为
几个寄存器的二进制表示方法。所以
mov reg, imm 机器指令为
10111XXX YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY
mov reg, mem 将内存数据移动到寄存器中 的机器指令怎么表示?
mov mem, reg 将寄存器中的数据移动到内存中,机器指令如何表示?
下图定义了变量num1, num2, num3
num1 地址为 0000 0000
num2 地址为 0000 0004
num3 地址为 0000 0008
下图为指令对应的机器码:
除去A1,A3开始的一个字节,剩下的4个字节分别为 十六进制4 和十六进制8,分别为num2的地址
和num3 的地址。
下面分析第一个字节A1和A3 规律:
可以得出结论,无论将内存数据移动到eax中,还是将eax中的数据移动到内存中,
最后的4个字节表示的都是内存的地址,第一个字节表示的不同,用来表示两种移动方式的区别。
总结规律如下:
mov eax, mem 10100001 YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY
mov mem , eax 10100011 YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY
下面图表表示了ebx,ecx,edx三种寄存器和内存数据移动指令
可以看出其他寄存器(eax, ecx, edx)和内存之间移动数据的操作指令大小为6字节,多出的为第二字节,
0D,15,1D。
第一字节8B表示从内存移动数据到寄存器,89表示从寄存器移动到内存,如下图所示:
第二个字节图表如下:
通过二进制可以看出 前两位都为 00, 中间三位为 001 ,010, 011 分别表示ecx, edx, ebx
最后三位为101, 其实这个字节不仅仅用于表示移动,还可以表示很多操作,因为ecx为循环控制,
ebx为基址寄存器, edx可用于存余数等等,所以 前两位为00,且最后三位为101,这个组合表示移动
操作。中间三位表示操作的寄存器是什么。
该字节概括为如下图所示:
mod 字段为00, 且r/m字段值为101,它表示地址模式数据置换,也就是指向内存地址模式。
add 指令 sub 指令
add指令很简单,给出图表读者自己分析。
字节转为二进制可以看出从右往左数第3到1位为第二个操作的寄存器,
从右往左数第6到4位表示第一个操作的寄存器, 两个寄存器操作模式为 第7~16位所表示。
movoffset 指令 lea 指令
num2 的值为5, 地址为 00000004, 分别将num2 的内容移动到esi和edi,再通过lea指令将num2地址
放入esi和edi
通过对比可以看出后四个字节都为 0000 0004 , 但是前两个字节是不一样的。将前两个字节展开
为二进制
由于mov esi, num2 是将num2数据存入esi,而 lea esi, num2 是将num2地址放入esi,所以
第一个字节的倒数第二位不同,第一个字节分别为10001011 , 10001101,mov和lea第二个字节是相同的。
下面对比两个lea指令前两个字节 ,第一个字节是相同的,第二个字节为 00 110 101 和 00 111 101
第二个字节中间三位不同,分别为110(esi), 111(edi)表示寄存器。
所以可以总结一下, mov 指令和 lea指令区别在于第一个字节,计算机用第一个字节区别mov和lea指令。
计算机用第二个字节中间三位区别lea指令操作的不同寄存器。
第二个字节和我们上面说过的:
mod 字段为00, 且r/m字段值为101,它表示地址模式数据置换,也就是指向内存地址模式。
下面看一下 mov esi, offset mem 和 mov edi, offset mem两条指令。
可以看出mov offset指令为5个字节,比 mov 和lea指令少了一个字节,因为mov offset仅仅在编译的时候加载地址,
所以不需要lea的第二个字节表示 数据移动操作。 mov offset是静态的。
mov offset 指令esi和 edi区别仅仅在第一个字节,展开后可以看到:
第一个字节的后三位 分别为 110(esi), 111(edi)。
可以得出结论mov offset 的指令第一个字节后三位区别esi还是edi,其余不变。
jmp指令
看一则jmp指令操作
jmp 指令 机器码为EB + 相对偏移地址 ,
如 jmp around 为EB 04 ,通知计算机跳转到当前指令指针位置+4字节的位置,
需要普及一个知识,当程序运行的时候,指令指针或者CPU中的指令指针指向下一条将要取到CPU中
被后续执行的指令。当运行到 jump around时,指令指针实际指向了 地址000000D8,指令 above:nop的位置,
EB 04指向 为 000 000 DB 加上4个字节地址即为 000 000 DC, 恰好是 around:nop指令地址。
符合逻辑。
下面看下 jump above指令会跳转到哪里。
EB FC 指令 FC 为 1111 1100 , 该数值为某个负数的补码,负数补码的计算规则为
符号位不变,其他位按位取反末位+1。
同样的道理,负数补码转为原码,符号位不变,按位取反末位+1
1111 1100 符号位不变,按位取反 1000 0011, 末位+1,
变为 1000 0100 表示-4.
jmp above 机器指令 EB FC 跳转到 指令指针地址向前移动四个字节的位置。
jmp above 指令运行时,指令指针指向下一条将要取出的指令位置,即 000 000 DC,
000 000 DC - 4 为 000 000 08,即 above:nop 的位置。
到此为止机器指令的知识介绍完毕,以后会介绍高级汇编和反汇编的知识,
汇编基础介绍告一段落。