第二章: 汇编版hello world


在本章,我们将以汇编版的“hello world",为大家介绍在IDE中如何编写,编译,测试和调试汇编程序,以及系统中关于内存寻址的相关理论。

2.1汇编版Hello world

汇编的程序结构主要包含代码段,数据段,堆栈段。下面举一个汇编版的hello world来了解汇编程序的结构构成(在汇编程序中,以“;”开始的语句是注释语句)。

 

;堆栈段

stack segment

db 100 dup(?)

stack ends

 

;数据段

data segment

szHello db 'hello,world',0dh,0ah,'$'

data ends

 

;代码段

code segment

;assumecsdsss段寄存器分别与codedatastack段对应

assume cs:code,ds:data,ss:stack

 

;程序开始执行

start:

mov ax,data

mov ds,ax

 

mov ah,9

mov dx,offset szHello

int 21h

 

mov ah,1

int 21h

 

mov ah,4ch

int 21h

;代码段结束

code ends

;程序结束

end start

 

在上面的汇编程序中,首先定义了一个堆栈段。堆栈段长100字节,这里db就是字节的意思,如果是用dw就是字,100 dup(?) 中的dup是重复的意思,这句话是重复100遍,一遍定义一个字节,每个字节里全填0

   

紧接着定义了一个数据段。定义了字符串szHello,其中”0dh”,”0ah”分别是回车键,换行键的ASCII码,'$'字符串的结束标志。

 

最后定义了一个代码段。指令”assume”确定csdsss,指向的逻辑段。汇编中,assume的作用是告诉汇编程序各个段寄存器应该与哪个逻辑段对应,该伪操作把程序中定义的各个段分配给段寄存器,使程序中使用的段名与段寄存器建立起对应关系。使得汇编程序清楚在计算地址时使用哪一个段地址但并不赋予该逻辑段的段地址。没有assume这行代码也可以,但其后的变量操作需要用段跨越前缀指明所在逻辑段。所以说assume语句是设定语句,是告诉汇编器在将源代码翻译成机器语言时,CPU的各个段寄存器如何与你定义的各个逻辑段相联系。比如ASSUME CS:CODE 这个例子CS段寄存器就选了CODE代码段。

 

start:

mov ax,data

mov ds,ax ;设置数据段的段地址ds

 

mov ah,9 ; ah中的9号功能表示要显示一行字符串

mov dx,offset szHello ;取得szHello的偏移地址,也就是在ds段中的偏移地址

int 21h

 

mov ah,4ch;退出 ah中的功能号4ch表示要返回DOS

int 21h ;调用DOS

code ends

end start

 

2.2汇编开发编译平台

开发和调试汇编程序的方式很多。具体来说有masmmasm32masm64以及VS等工具都可以用来开发和调试汇编程序。

 

16位汇编:masm,dosbox

1.         安装masm包(含masm,link,debug等工具):c:\masm

2.         安装并启动dosbox0.74

3.         mount c: c:\masm

4.         切换到masm安装路径: c:

5.         编辑a.asm汇编代码

6.         masm a.asm

7.         link a.obj

           运行a.exe

8.         debug a.exe

9.         u反汇编a.exe代码

 

32位汇编:masm32

1.         demo拷贝到masm32\examples\

2.         启动QEDITOR.EXE打开.asm文件

3.         Project -> Assemblier ASM file

4.         Project -> Link OBJ file

5.         Project -> Run program

32位系统类似,64位汇编可以使用masm64工具。

 

VS中嵌入汇编:

VS中,可以使用__asm{}来嵌入编译和调试汇编程序,比如:

int strcmp_asm(char *s1, char *s2)

{

     __asm{

            mov esi,s1;

            mov edi,s2;

            L1:

            lodsb;lodsb:[esi]-->al,esi=esi+1

            scasb;scasb:al-[edi],edi = edi+1

            jne L2;

            test al,al;判断esi中是否到达字符串末尾

            jne L1;

            xor eax,eax

                   jmp L3;

            L2:sbb eax,eax;

                   or al,1;

            L3:

 

     }

}

 

int strcmp_asm2(char *s1, char *s2,size_t len)

{

     __asm{

        mov esi,s1;

        mov edi,s2;

        mov ecx,len;

 

        repz cmpsb;(ecx)!=0&&(esi-edi)==0

 

        jecxz L1;ecx==0,jmp

        js L2;(esi-edi)<0

        jns L3;(esi-edi)>0

 

L1:

        js L2;

        jnz L3;

        xor eax,eax;

        jmp L4;

L2:

        mov eax,-1;

        jmp L4;

L3:

        mov eax,1;

L4:

 

     }

}

 

int _tmain(int argc, _TCHAR* argv[])

{

 

       printf("%d\n", strcmp_asm2("hello","hellx",strlen("hello")));

       printf("%d\n", strcmp_asm("hcllo world", "hbllo world"));

 

       return 0;

}

   

由此可以看出,使用VS开发与调试汇编程序,实际上是最方便的。所以,这也是笔者为大家推荐的方式。

2.3内存寻址模式

程序的内存寻址是一个重要的概念。现在来具体介绍一下这方面的概念。比如内存地址可以分为逻辑地址,线性地址和物理地址,以及内存寻址模式中的实模式、保护模式,分段模型以及平坦模型。

2.3.1 逻辑地址,线性地址,物理地址

1.逻辑地址是编译器生成的,我们使用在linux环境下,使用C语言指针时,指针的值就是逻辑地址。对于每个进程而言,他们都有一样的进程地址空间,类似的逻辑地址,甚至很可能相同。逻辑地址由段地址+段内偏移组成

2.线性地址是由分段机制将逻辑地址转化而来的,如果没有分段机制作用,那么程序的逻辑地址就是线性地址了。

3.物理地址是CPU在地址总线上发出的电平信号,要得到物理地址,必须要将逻辑地址经过分段,分页等机制转化而来。

 

实模式和保护模式相对,实模式运行于20位地址总线,保护模式则启用了32位地址总线,地址使用的是虚拟地址,引入了描述符表;虽然二者都引入了段这样一个概念,但是实模式的段是64KB固定大小,只有16个不同的段,CS,DS等存储的是段的序号。保护模式则引入了GDTLDT段描述符表的数据结构来定义每个段。

扁平模型和分段模型相对,区别在于程序的线性地址是共享一个地址空间还是需要分成多个段,即为多个程序是同时运行在同一个CSDS的范围内还是每个程序都拥有自己的CSDS:也就是说前者(flat)指令的逻辑地址要形成线性地址,不需要切换CSDS;后者的逻辑地址,必须要经过段选择子去查找段描述符,切换CSDS,才能形成线性地址。

X86体系结构下,使用的较多的内存寻址模型主要有三种:

1. 实模式扁平模型 real mode flat model

2. 实模式分段模型 real mode segment model

3. 保护模式扁平模型 protected mode flat model

2.3.2实模式分段模型

在实模式里,20位地址总线,16位的寄存器无法表示,一个基址寄存器+一个段寄存器联合起来则可以表示更大的一个地址空间。于是发明了这种段寄存器左移4+基址寄存器用以间接寻址。

20根地址线,表示 0x00000 - 0xfffff这个范围的地址(即1M 而寄存器16位,还有4位怎么办?于是8086CPU1MB的存储器空间分成许多逻辑段,每个段最大限制为64KB(为了能让16位寄存器寻址,2^20=2^10*2^10=2^10*2^6*2^4==16*64K), 段地址就是逻辑段在主存中的起始位置。为了能用16位寄存器表示段地址,8086规定段地址必须是模16地址,即为xxxx0H形式,省略低40,段地址就可以用16位数据表示,它通常被保存在16位的段寄存器中。存单元距离段起始位置的偏移量简称偏移地址,由于限定每段不超过64KB,所以偏移地址也可以用16位数据表示。物理地址:在1M字节的存储器里,每一个存储单元都有一个唯一的20位地址,称为该存储单元的物理地址,把段地址左移4(因为段地址低4位都是零)再加上偏移地址就形成物理地址。Seg<<4+Offset 对于 8086/8088 运行在实模式的程序,其实就是运行在实模式分段模型中。

对于不同的程序,有不同的CSDS值,每个程序的段起始地址都不同。对于这样的程序而言,偏移地址16位的特性决定了每个段只有64KB大小。

2.3.3保护模式平坦模型

Linux Window XP/7采用的内存寻址模型,Linux中,段主要分为4种,即为内核代码段,内核数据段,用户代码段,用户数据段。对于内核代码段和数据段而言,CS,DS的值是0xC00000000,而用户代码和数据段的CS,DS的值是0x00000000 CPU运行于32位模式时,不管怎样,寄存器和指令都可以寻址整个线性地址空间,所以根本就不需要像16位系统中为了寻址的需要再去使用基地址。基址可以设为一个统一的值,此即为平坦模式。