数据段

一般一个程序想要使用内存空间,有两种方法,在程序加载的时候系统分配或在需要使用的时候向系统申请,我们先考虑第一种情况。所以我们应事先将所需的数据存入内存中的某一段中,但我们又不可以随意的指定内存地址,以下面的求8个数据累加和的代码为例:

  1. assume cs:code
  2. code segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. mov bx,0
  5. mov ax,0
  6. mov cx,8
  7. s:add ax,cs:[bx]
  8. add bx,2
  9. loop s
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end

代码第一行的dw是定义字类型数据,define word的意思。这里定义了8个字类型数据,占16字节。由于是在程序最开始定义的dw,所以数据段的偏移地址为0,也就是说第一个数据0123h的地址是CS:[0]第二个0456h的地址是CS:[2]以此类推。

所以这个程序加载之后CS:IP指向的是数据段的第一个数据,我们要是想成功执行,需要把IP置10,指向第一条指令mov bx,0,所以我们想要直接执行(不在Debug中调整IP)的话,需要指定程序开始的地方:

  1. ···
  2. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  3. start:mov bx,0
  4. ···
  5. code ends
  6. end start

在第一条指令前加start,后面的end变成end start,end除了通知编译器程序在哪里结束之外,也可以通知程序的入口在哪,也就是第一条语句,在这里编译器就知道了mov bx,0是程序的第一条指令。也就是说,我们想要CPU从何处开始执行程序,只要在源程序中使用end 标号指定就好了。

所以有如下框架:

  1. assume cs:code
  2. code segment
  3. ···数据···
  4. start:
  5. ···代码···
  6. code ends
  7. end start

栈段

看下面一段使8个数逆序存放的代码:

  1. assume cs:codesg
  2. codesg segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
  5. start:mov ax,cs
  6. mov ss,ax
  7. mov sp,30h
  8. mov bx,0
  9. mov cx,8
  10. s:push cs:[bx]
  11. add bx,2
  12. loop s
  13. mov bx,0
  14. mov cx,8
  15. s0:pop cs:[bx]
  16. add bx,2
  17. loop s0
  18. mov ax,4c00h
  19. int 21h
  20. codesg ends
  21. end start

在定义了8个字型数据之后,又定义了16个取值为0的字型数据,用作栈空间。所以dw这个定义不仅仅用来定义数据,也可以用来开辟内存空间留给之后的程序使用。

数据,代码,栈的程序段

在8086CPU中,一个段的长度最大为64KB,所以如果我们将数据或栈空间定义的比较大,就不能像前面一样编程了。我们需要将代码,数据,栈放入不同的段中:

  1. assume cs:code, ds:data, ss:stack
  2. data segment vstart = 0
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. data ends
  5. stack segment
  6. dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
  7. stack ends
  8. code segment
  9. start: mov ax,stack
  10. mov ss,ax
  11. mov sp,20h ;设置栈顶ss:sp指向stack:20
  12. mov ax,data
  13. mov ds,ax ;ds指向data
  14. mov bx,0 ;ds:bx 指向data段中地第一个单元
  15. mov cx,8
  16. s: push [bx]
  17. add bx,2
  18. loop s ;以上将data段中的015单元中的8个字型数据依次入栈
  19. mov bx,0
  20. mov cx,8
  21. s0: pop [bx]
  22. add bx,2
  23. loop s0 ;以上依次出栈8个字型数据到data段地015单元中
  24. mov ax,4c00h
  25. int 21h
  26. code ends
  27. end start

注意: 所有的segment的地址都是从程序开始(也就是从0开始)处计算的,在nasm编译器里面可以用:

  1. section.段名称.start ;来表示段的地址

不过segment内的地址会有点不一样。如果segment声明处有vstart=xx,则段内地址是从xx开始计算,如vstart=0x7c00,则第一条指令地址就是0x7c00,如果vstart=0,则第一条指令地址就是0。如果没有则是从整个程序头部开始计算。

我们可以这样在写代码时就将程序分为几个段,这段代码中,mov ax,data的意思是将data段的段地址送入ax寄存器。但我们不可以使用mov ds,data这样是错误的,因为在这里data被编译器视为一个数值。

在这里将数据命名为data,代码命名为code,栈命名为stack只是为了方便阅读,CPU并不能理解,和start,s,s0一样,只在源程序中使用。而assume cs:code,ds:data,ss:stack这段代码也并不能让CPU的cs,ds,ss指向对应的段,因为assume是伪指令,CPU并不认识,它是由编译器执行的。源程序中end start语句指明了程序的入口,在这个程序被加载后,CS:IP被指向start处,开始执行第一条语句,这样CPU才会将code段当做代码执行。而当CPU执行

  1. mov ax,stack
  2. mov ss,ax
  3. mov sp,20h

这三条语句后才会将stack段当做栈空间开使用。也就是说,CPU如何区分哪个段的功能,全靠我们使用汇编指令对ds,ss,cs寄存器的内容设置来指定。