汇编的程序结构主要包含代码段,数据段,堆栈段。下面举一个汇编版的hello world来了解汇编程序的结构构成(在汇编程序中,以“;”开始的语句是注释语句)。
;堆栈段
stack segment
db 100 dup(?)
stack ends
;数据段
data segment
szHello db 'hello,world',0dh,0ah,'$'
data ends
;代码段
code segment
;assume将cs,ds,ss段寄存器分别与code,data,stack段对应
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”确定cs,ds,ss,指向的逻辑段。汇编中,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
开发和调试汇编程序的方式很多。具体来说有masm,masm32,masm64以及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开发与调试汇编程序,实际上是最方便的。所以,这也是笔者为大家推荐的方式。
程序的内存寻址是一个重要的概念。现在来具体介绍一下这方面的概念。比如内存地址可以分为逻辑地址,线性地址和物理地址,以及内存寻址模式中的实模式、保护模式,分段模型以及平坦模型。
1.逻辑地址是编译器生成的,我们使用在linux环境下,使用C语言指针时,指针的值就是逻辑地址。对于每个进程而言,他们都有一样的进程地址空间,类似的逻辑地址,甚至很可能相同。逻辑地址由段地址+段内偏移组成
2.线性地址是由分段机制将逻辑地址转化而来的,如果没有分段机制作用,那么程序的逻辑地址就是线性地址了。
3.物理地址是CPU在地址总线上发出的电平信号,要得到物理地址,必须要将逻辑地址经过分段,分页等机制转化而来。
实模式和保护模式相对,实模式运行于20位地址总线,保护模式则启用了32位地址总线,地址使用的是虚拟地址,引入了描述符表;虽然二者都引入了段这样一个概念,但是实模式的段是64KB固定大小,只有16个不同的段,CS,DS等存储的是段的序号。保护模式则引入了GDT和LDT段描述符表的数据结构来定义每个段。
扁平模型和分段模型相对,区别在于程序的线性地址是共享一个地址空间还是需要分成多个段,即为多个程序是同时运行在同一个CS,DS的范围内还是每个程序都拥有自己的CS,DS:也就是说前者(flat)指令的逻辑地址要形成线性地址,不需要切换CS,DS;后者的逻辑地址,必须要经过段选择子去查找段描述符,切换CS,DS,才能形成线性地址。
X86体系结构下,使用的较多的内存寻址模型主要有三种:
1. 实模式扁平模型 real mode flat model
2. 实模式分段模型 real mode segment model
3. 保护模式扁平模型 protected mode flat model
在实模式里,20位地址总线,16位的寄存器无法表示,一个基址寄存器+一个段寄存器联合起来则可以表示更大的一个地址空间。于是发明了这种段寄存器左移4位+基址寄存器用以间接寻址。
20根地址线,表示 0x00000 -
0xfffff这个范围的地址(即
对于不同的程序,有不同的CS,DS值,每个程序的段起始地址都不同。对于这样的程序而言,偏移地址16位的特性决定了每个段只有64KB大小。
Linux, Window XP/7采用的内存寻址模型,Linux中,段主要分为4种,即为内核代码段,内核数据段,用户代码段,用户数据段。对于内核代码段和数据段而言,CS,DS的值是0xC00000000,而用户代码和数据段的CS,DS的值是0x00000000 当CPU运行于32位模式时,不管怎样,寄存器和指令都可以寻址整个线性地址空间,所以根本就不需要像16位系统中为了寻址的需要再去使用基地址。基址可以设为一个统一的值,此即为平坦模式。