欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

汇编语言002_课上笔记

程序员文章站 2024-01-29 19:43:16
...

课后作业讲解

注: 在16位汇编中, 并使用sp作为寄存器寻址, 因此, 下面的代码并不能直接运行. 但在32位汇编中(将所有寄存器改成32位之后)是没有问题的.

int a=10;
int b=20;
int c=4;
a = (a << c) * (a>>c) / ((c ^ a) | (c ^b )) - (++c + --a)
部分示例:
mov ax,10;
mov bx,20;
mov cx,4;

用到的汇编指令: 
shl, shr , idiv , xor , or , sub , inc, dec
优先级关系:
;1. a << c => 中间结果1
push ax 		   ; 将ax的值保存到栈中
shl ax,cl 		   ; ax <<= cl;
xchg ax,[sp]       ; ax是运算结果,[sp]以前的值

;2. a >> c => 中间结果2
push ax			   ; 保存ax的值
shr ax , cl 	   ; ax <<= cl
xchg ax , [sp]  


;3. c ^ a  => 中间结果3
push cx
xor cx , ax;
xchg cx,[sp]

;4. c ^ b  => 中间结果4
push cx
xor cx , bx
xchg cx,[sp]

;5.  (中间结果3 | 中间结果4) => 中间结果5
xchg ax,[sp+0] ; ax等于中间结果4, [sp+0]=旧ax的值
or ax ,[sp+2]
xchg ax,[sp+0]; sp+0 保存的是中间结果5, ax值被恢复了


;6. ++c 
inc cx;

;7. --a
dec ax

;8. c+a => 中间结果6
push cx
add cx , ax;
xchg cx,[sp]

;9. 中间结果1 * 中间结果2 / 中间结果5 - 中间结果6
mov ax , [sp+8]
mul [sp+6]      ; ax*= 中间结果2
idiv [sp+2]
sub ax,[sp+0] 

; 恢复栈
add sp , 0ch 

代码运行时的栈布局:

  • 中间结果6

  • 中间结果5; 第三次push和xchg的结果和ax交换了,然后又保存中间结果5

  • 中间结果3 ; 第三次push和xchg

  • 中间结果2 ; 第二次push,然后交换之后的值

  • 中间结果1 ; 是第一次push,然后交换之后的值

寻址方式

  • 立即数寻址

  • 寄存器寻址

  • 存储器寻址

    • 直接寻址 : mov ax, [ 01000h ]; 直接在[]内给出一个内存地址

    • 寄存器间接寻址: mov ax ,[si]; 在[]以寄存器的值给出内存地址.

    • 寄存器相对寻址: mov ax,[si+0ch][]以寄存器的值和一个数相加之后作为内存地址.

      struct MyStruct{
          int n1;
          char ch;
          int n2;
      };
      MyStruct stc;
      stc.n1 = 0;
      stc.n2 = 10;
      //假设stc的内存首地址是0x1000
      // 以汇编形式访问结构体字段:
      mov bx , 0x1000; // bx保存了结构体首地址
      mov [bx+0] , 0; //stc.n1
      mov [bx+8] , 10;//stc.n2
      
    • 基址变址寻址: mov ax,[si+bx] 使用两个寄存器相加之和作为内存地址

      char szBuff[10];
      for(int i = 0; i<10;++i){
          szBuff[i] = 0;
      }
      // 假设szBuff首地址是0x1000
      mov si , 0x1000;
      xor bx,bx;
      
      for(int i =0;i<10;++i){
          // szBuff[i] = 0;
          mov [si+bx] , 0; 
      	inc bx;
      }
      
    • 相对基址变址寻址: mov ax,[si+bx+0ch] 使用[]内的表达式的相加之和作为内存地址.

    • 在16位汇编中, 要使用存储器寻址的时候, 如果希望用寄存器寻址, 那么只能使用si,di,bx,bp寄存器, 然后这些寄存器不能任意组合.

条件跳转指令

有有符号跳转和无符号跳转之分.

常见条件跳转指令:

  • 有符号跳转:
    • jg 大于
    • jge 大于等于
    • jl 小于
    • jle 小于等于
  • 无符号跳转
    • ja 大于, cf0 且 zf0则跳转
    • jae 大于等于 , cf ==0 则跳转
    • jb 小于, cf==1则跳转
    • jbe 小于等于, cf1 或 zf1则跳转
  • 不区分符号跳转
    • je zf==1 则跳转
    • jne zf==0 则跳转

32位汇编

第一个汇编项目


.386 ;告诉汇编器, 使用32位汇编的语法来编译
.model flat , stdcall ;默认使用平坦模式,默认使用stdcall的调用约定
option casemap:none 


.code ; 定义一个代码段

sldkfjlaskdjfmain: 
	ret 
    	
end sldkfjlaskdjfmain; 指定程序入口点
end ; 结束代码段

masm汇编器的语法

  1. 每个asm源码文件中都以下面的指令打头:

    .386 ;告诉汇编器, 使用32位汇编的语法来编译
    .model flat , stdcall ;默认使用平坦模式,默认使用stdcall的调用约定
    option casemap:none 
    
  2. 必须自己定义一个代码段, 定义代码段使用.code伪指令 , 汇编指令就写在.codeend之间

  3. 程序必须要有一个入口点.在C语言中, 就固定了是main函数. 在masm里面, 可以在代码段中使用end 标签的方式来指定入口点.

  4. 数据的定义必须放在数据段, 数据段使用.data指令来定义.

    .data ; 定义数据段
    .const ; 定义常量数据(不可修改的数据)
    .code ; 定义代码段
    
    1. 在数据段中定义数据, 可以使用d系列指令

      .data 
      ch db 'a'         ; 相当于 char ch='a';
      
      buff db 0,0,0,0 ; 相当于 char buff[]={0,0,0,0};
      
      str db "hello" , 0; 相当于 char str[]={'h','e','l','l','o' , 0 }; 也就是说,这里不会自动加上字符串结束符'\0'
      
      str2 db "hello\n" , 0; 在masm中没有转义字符.
      str2 db "hello", 0ah , 0 ; 0ah是'\n'
      
      ; 其它类型
      var1 dw 100 ; word类型
      var2 dd 100 ; dword类型
      
      ; dup用于重复定义数据 , dup前是一个重复的次数, dup圆括号内是需要重复的初始化值.
      arr dd 100 dup(0) ; int arr[100]={0};
      
      

汇编版本的helloworld


.386 ;告诉汇编器, 使用32位汇编的语法来编译
.model flat , stdcall ;默认使用平坦模式,默认使用stdcall的调用约定
option casemap:none ; 不区分大小写

; 包含名为`msvcrt.inc`头文件(c语言的所有库函数)
include msvcrt.inc
; 包含库文件
includelib msvcrt.lib

;include windows.inc
;include user32.inc
;includelib user32.lib

.data
 g_str db "hello world",0dh,0ah, 0 ; \r\n==0d0a

.code ; 定义一个代码段

main: 
	push offset g_str;
	call crt_printf
	add esp , 4
    	ret 
    	
end main; 说明程序入口点
end ; 结束代码段

汇编程序基础

三大程序结构

  • 顺序结构
  • 选择结构
    • 在c中, if else , switch
  • 循环结构

模拟选择结构

模拟if-else

int n =0;
scanf("%d",&n);

if( n == 1){
    printf("星期一\n");
}
else if(n==2){
    printf("星期二\n");
}
else if(n==3){
    printf("星期三\n");
}

汇编版本

.data 
n dd 0 ; 定义一个全局变量, 名字为n

.code
_main:
	cmp n , 1
	je _FLAG1
	cmp n , 2
	je _FLAG2
	cmp n , 3
	je _FLAG3
_FLAG1:
	printf("星期一\n");  
    jmp _ENDIF
_FLAG2:	
	printf("星期二\n");
	jmp _ENDIF
_FLAG3:
    printf("星期三\n");
_ENDIF:
end _main
end

模拟switch-case

switch( n )
{
    case 1: printf("星期1\n");break;
    case 2: printf("星期2\n");break;
    case 3: printf("星期3\n");break;
}

汇编版本1(和if-else的一样)

汇编版本1 : 使用跳转表


.386 ;告诉汇编器, 使用32位汇编的语法来编译
.model flat , stdcall ;默认使用平坦模式,默认使用stdcall的调用约定
option casemap:none ; 不区分大小写

; 包含名为`msvcrt.inc`头文件(c语言的所有库函数)
include msvcrt.inc
; 包含库文件
includelib msvcrt.lib

;include windows.inc
;include user32.inc
;includelib user32.lib

.data
 g_str db "hello world",0dh,0ah, 0 ; \r\n==0d0a
 str1 db  "星期一",0dh,0ah,0
  str2 db  "星期2",0dh,0ah,0
  str3 db  "星期3",0dh,0ah,0
 
.code ; 定义一个代码段

main: 
.code
	jmp being
	jmptable dd _FLAG1,_FLAG2,_FLAG3 ; 在code段定义数据
being:
	mov eax , 1 ; ; 
	dec eax;
	jmp [jmptable+eax*4]; 根据eax的值,来跳转不同的位置.

_FLAG1:
	invoke crt_printf, offset str1;  
    jmp _ENDIF
_FLAG2:	
	invoke crt_printf , offset str2;
	jmp _ENDIF
_FLAG3:
    invoke crt_printf ,offset str1;
_ENDIF:

end main; 说明程序入口点
end ; 结束代码段

模拟循环结构

使用条件跳转模拟循环

int i =0;
while (i < 10)
{
    ++i;
}

汇编版本1:

    xor eax,eax ; 使用eax寄存器作为i
    
_WHILE:
    cmp eax , 10 ;
    jge _ENDWHILE
	inc eax
	jmp _WHILE
_ENDWHILE:

汇编版本2 : loop 循环, 该指令使用ecx作为默认寄存器, 保存着循环次数, loop指令执行之后, 会判断ecx的值是否等于0 , 如果等于了,就不会跳转, 如果没有等于, 就先将ecx递减1, 然后跳转

	mov ecx , 10; 
_WHILE:
	
 	loop _WHILE ; 

函数结构

-堆栈平衡:函数调用完成后,要返回所有使用过的栈空间,也就是要退栈。
-函数调用约定:函数调用约定规定了函数参数的入栈方式和堆栈的平衡方式。

  • 函数调用和函数返回语句call,ret

    • call 目标地址 - 调用函数的指令
      • 会将call指令的下一条指令的地址push到栈中.
      • 跳转到目标地址.
    • ret 返回指令
      • 其实就是pop eip ,call指令将一个返回地址保存到栈中, ret就默认把栈中的地址取出设置到eip这样就能回调函数的调用点了.
      • ret 字节数 - 返回时,顺便平衡指定字节栈空间.
  • 定义函数

    • 通过ret指令来回到函数的调用点.
  • 调用函数

    • 函数的传参是通过栈来完成的.

      • 调用函数之前, 先将实参压入栈中.

      • 进入函数之后, 就可以从栈中取出参数了.

      • 在传参的时候, 是从右往左依次将参数入栈,还是从左往右,需要有一个函数调用约定

        调用约定名 传参顺序 栈平衡者
        _cdecl - C调用约定 从右往左 函数外部
        _stdcall - 标准调用约定 从右往左 函数内部
        _thiscall - 对象调用 从右往左,this指针保存到ecx寄存器 函数内部
        fastcall - 快速调用约定 前两个参数通过ecx,edx来传递, 后面的参数从右往左依次入栈传递 函数内部
    • 通过call + 函数地址完成调用

  • 在函数内部定位栈中的参数

  • 使用局部变量

  • 栈空间布局

#lea指令和offset指令
lea 是机器指令,offset 是伪指令。
LEA BX, BUFFER ;在实际执行时才会将变量buffer的地址放入bx
–所谓伪指令是在编译阶段执行的,有编译器执行。
–机器指令是在运行阶段,由cpu执行的。
MOV BX, OFFSET BUFFER ;在编译时就已经计算出buffer的地址为4300(假设),然后将上句替换为: mov bx,4300

lea可以进行比较复杂的计算,比如lea eax,[esi+ebx4],把ebx的值4,加上esi的值,存入eax中。
mov就不行了。

OFFSET只能取得用"数据定义伪指令"定义的变量的有效地址,不能取得一般操作数的有效地址(摘自80x86汇编语言程序设计教程)
MOV BX,OFFSET [BX+200]这句是错误的 应该用LEA BX,[BX+200]
lea eax,[ebp]
说明: eax得到ebp指向的堆栈内容的偏移地址, 和寄存器ebp的值是相同的