Hello World!
今天我们来编写第一个汇编程序,还是经典打印Hello world,这里我们在windows环境下一个emu8086软件,编写打印hello world如下:

打印效果如上所示,完整代码如下:
; 提醒开发者每个段的含义,并无实际含义
assume cs:code, ds:data
; -----定义 数据段 begin -----
data segment
age db 20h ;d代表define,b代表一个字节 age是这个数据的名字
no dw 3000h ;定义一个字,占两个字节
db 10 dup(10) ; 生成10个连续的10
string db 'Hello World!$' ; $代表字符串结束
data ends
; ----- 数据段 end -----
; ----- 代码段 begin -----
code segment
start:
; 手动设置ds的值
mov ax, data
mov ds, ax
; 打印
mov dx, offset string
; offset string代表string的偏移地址
; dx表示在ds段中的偏移地址
mov ah, 9h;功能号9h代表在屏幕上显示字符串
int 21h
; 退出程序
mov ax, 4c00h
int 21h
code ends
; ----- 代码段 end -----
; 编译结束,start是程序入口
; start所在的段就是代码段
; 所以cs的值就是code段的段地址
; 相当于cs的值已经自动设置完毕
end start
栈的应用
函数的实现依赖栈,只有把栈弄清楚明白,再来看函数调用本质,会容易得多,比如实现一个需求交换两个数据1122h和334h,这里我们用栈来实现如下,emu8086可以单步调试,可以查看各个寄存器的值以及内存情况,值得你亲手试一试,非常好用,对整个汇编代码结构有所深入认识后,我们继续研究,实现如下:
assume cs:code, ss:stack
; 栈段
stack segment
db 10 dup(8)
stack ends
; 代码段
code segment
start:
; 手动设置栈段ss
mov ax, stack
mov ss, ax
mov ax, 1122h ;将1122传给ax
mov bx, 3344h ;将3344传给bx
mov sp,10 ;将栈顶指针指向栈中最高地址的下一个地址
push ax ;将ax中的1122入栈,并且栈顶指向22
push bx ;将bx中的3344入栈,栈顶指向44
pop ax ;从栈中弹出数据,将栈顶的数据3344赋值给ax,sp=sp-2
pop bx ;从栈中弹出数据,将栈顶的数据1122赋值给bx,sp=sp-2
; 退出
mov ax, 4c00h
int 21h
code ends
end start
现在我们知道了栈现先进后出,栈顶,栈顶指针,栈底的概念,现在我们有足够的知识来深入了解函数的调用原理,我们知道函数有几个元素:函数名,返回值,参数,返回值,局部变量。一个相对完整的函数代码如下:
int print(int number)
{
int a = 1;
int b = 2;
int c = a + b;
return c;
}
函数调用原理
我们先来看看我们对函数的一般认识:函数栈中局部变量自动回收,局部变量从高地址到地址分配,递归调用函数,如果没有函数出口会导致栈溢出等。其实这些说法都是对的,但是对函数调用以及本质还是模模糊糊的,然而从汇编走一遍流程,就完全明白了。这里直接举例一个函数调用的完整流程汇编代码如下:
assume cs:code, ds:data, ss:stack
; 栈段
stack segment
db 100 dup(0)
stack ends
; 数据段
data segment
db 100 dup(0)
data ends
; 代码段
code segment
start:
; 手动设置ds、ss的值
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
mov si, 1
mov di, 2
mov bx, 3
mov bp, 4
; 业务逻辑
push 1
push 2
call sum
add sp, 4
; 退出
mov ax, 4c00h
int 21h
; 返回值放ax寄存器
; 传递2个参数(放入栈中)
sum:
; 保护bp
push bp
; 保存sp之前的值:指向bp以前的值
mov bp, sp
; 预留10个字节的空间给局部变量
sub sp, 10
; 保护可能会用到的寄存器
push si
push di
push bx
; 给局部变量空间填充int 3(CCCC)
; stosw的作用:将ax的值拷贝到es:di中,同时di的值会+2
mov ax, 0cccch
; 让es等于ss
mov bx, ss
mov es, bx
; 让di等于bp-10(局部变量地址最小的区域)
mov di, bp
sub di, 10
; cx决定了stosw的执行次数
mov cx, 5
rep stosw
; rep的作用:重复执行某个指令(执行次数由cx决定)
; -------- 业务逻辑 - begin
; 定义2个局部变量
mov word ptr ss:[bp-2], 3
mov word ptr ss:[bp-4], 4
mov ax, ss:[bp-2]
add ax, ss:[bp-4]
mov ss:[bp-6], ax
; 访问栈中的参数
mov ax, ss:[bp+4]
add ax, ss:[bp+6]
add ax, ss:[bp-6]
; -------- 业务逻辑 - end
; 恢复寄存器的值
pop bx
pop di
pop si
; 恢复sp
mov sp, bp
; 恢复bp
pop bp
ret
code ends
end start
通过整理得出函数的调用流程如下:
1. push 参数到函数栈
2. push 函数的返回地址,这个操作系统自动push,便于执行ret后找到地址继续执行代码,其实就是这个地址是下一条指令代码的地址。
3. push bp 保留bp之前的值,方便以后恢复
4. mov bp, sp 保留sp之前的值,方便以后恢复
5. sub sp,空间大小,利用sp分配空间给局部变量,此时局部sp指向局部变量
6. push 保护可能要用到的寄存器入栈
7. 使用CC(int 3)填充局部变量,每个平台处理的方式可能不一样
8. 函数内部执行业务逻辑
9. 如果保护了寄存器,恢复寄存器之前的值
10. mov sp, bp (恢复sp之前的值)
11. pop bp (恢复bp之前的值)
12. ret (将函数的返回地址出栈,执行下一条指令)
13. 恢复栈平衡 (add sp,参数所占的空间)
这个流程对函数调用函数,递归调用也实用,这里的难点是理解sp和bp的作用,分配局部变量是通过sub sp方式分配局部变量,bp则是指向函数返回地址的附近地址,访问函数参数是bp+,访问局部变量是bp-,此时bp和sp保护一段内存区域,保护寄存器的作用是如果再调用函数,当上一个函数返回,bp和sp也需要相关恢复,否则会指向错误。下面通过图示再来详细说下:

这是一个函数调用后还没恢复栈平衡的栈的结构,注意bp的指向和sp的指向,这里说bp以前的值其实就是上一个函数的返回地址的附近地址,这个函数返回后应该回到上一个函数。
这是一个完整的典型的函数调用原理,有的函数没有参数,有的没有局部变量,有的没有保护寄存器的操作,而且在Mac中AT&T汇编或者ARM汇编中,函数参数都是用专门寄存器存储,无需压栈,但是深入了解了函数调用原理,其它都是类似, 本篇文章,内容比较偏难,希望大家能理解,祝进步!
网友评论