婚庆婚车强哥 發表於 2020-4-15 00:14:00

汇编语言入门学习 | 2 - 汇编语言代码基本结构

<h2 id="从一个例子开始">从一个例子开始</h2>
<p>根据个人习惯,我更愿意从一个实例开始某种语言的学习。<br><br>
<br>这里以一个 16 位汇编程序为例:<br>我们在 xp 虚拟机中新建文件 hello.asm,用记事本编辑:</p>
<pre><code class="language-Assembly"> 1 data segment
2 abc db "hello, world!", 0Dh, 0Ah, "$"
3 data ends
4 ;这是一条注释
5 code segment
6 assume cs:code, ds:data
7 main:
8   mov ax, data
9   mov ds, ax
10   mov ah, 9
11   mov dx, offset abc
12   int 21h
13   mov ah, 4Ch
14   int 21h
15 code ends
16 end main
</code></pre>
<p><br>将其放在 \Masm 目录中。该目录中同时包含了 LINK.EXE 以及 MASM.EXE。我们在 command 中进入对应目录,输入指令:</p>
<pre><code>masm hello;
link hello;
hello
</code></pre>
<p>我们通过运行 masm.exe 来编译 hello[.asm],然后通过 link.exe 来连接 hello[.obj],最后运行 hello[.exe]。结果显示: <strong>hello, world!</strong> 。<br><br>
<br>下面,我们对代码进行逐句解析:<br></p>
<ul>
<li>
<p><strong>段</strong></p>
<ul>
<li>
<p>对于 8086PC 机,在编程时可以根据需要将一组内存单元定义为一个段(机器语言代码也存储在内存中)。例如第 1 行和第 5 行就分别定义了名为 data 的段和名为 code 的段。第 3 行和第 15 行分别是这两个段的结束。</p>
</li>
<li>
<p>8086CPU 要求每个段的容量不能超过 64KB。这与计算机的寻址方式有关:</p>
<ul>
<li>计算机对内存的编码是线性的。例如内存为 256M,则地址就应该为 0~(256M-1) 。这个地址称为 <strong>物理地址</strong>&nbsp;或 <strong>绝对地址</strong>&nbsp;。</li>
<li>8086CPU 可以传送 20 位的地址 0~(1M-1),但是由于 8086CPU 是 16 位结构的,因此如果采用简单的方法传递物理地址,那么它的寻址能力只有 0~(64K-1)。因此 8086CPU 采用两个 16 位地址合成的方法来形成一个 20 位的物理地址。比如 12ABh:34DEh 就是一个地址(其中 12ABh 表示 16 进制下的 12AB。汇编语言中用末尾的一个 h 来表示 16 进制数。汇编语言中数字的表示不区分大小写。如果一个 16 进制数是字母开头的,则需要在它前面增写一个 0,如 ABCDh 应写为 0ABCDh,因为字母开头的字符串表示的是变量的名称),它由两个 16 位的地址组成,分别称为 <strong>段地址</strong>&nbsp;(Segment) 和 <strong>偏移地址</strong>&nbsp;(Offset)。这样表示的地址称为 <strong>逻辑地址</strong>&nbsp;。</li>
<li>地址加法器采用 <strong>物理地址 = 段地址 * 16 + 偏移地址</strong>&nbsp;的方法合成物理地址。</li>
<li>需要说明的是,同一个物理地址可以表示成多个逻辑地址。如 123BC = 123B:000C = 122A:011C。</li>
<li>因此,我们如果要用 段地址:偏移地址 的方式寻址,偏移地址的范围为 0h~FFFFh,即 10000h 个字节,即 64KB。</li>
</ul>
</li>
<li>
<p><strong>伪指令</strong></p>
<ul>
<li>在汇编语言源程序中,包含 2 种指令,一种是汇编指令,一种是伪指令。汇编指令是与机器码一一对应的,而伪指令由编译器来执行,编译器会进行相关的编译工作。</li>
<li>例如,segment 和 ends 就是一对成对使用的伪指令。</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>定义数组</strong></p>
<ul>
<li>
<p>第 2 行 <code>abc db "hello, world!", 0Dh, 0Ah, "$"</code> 定义了一个字节类型的变量(db: define byte,byte 类型实际上等价于 C 语言中的 char 类型),名为 abc,内容为 <code>"hello, world!", 0Dh, 0Ah, "$"</code> ,相当于 C 语言中的 <code>char abc[] = "hello, world!\x0D\x0A$";</code> ,即逗号隔开的内容会被连接成一个变量。其中 0Dh, 0Ah 分别是回车(光标回到行首)和换行(光标向下移动一行)的 ASCII 码。在汇编语言中,$ 是字符串结束的标志。</p>
</li>
<li>
<p>我们可以通过 <code>ans db 100 dup(0)</code> 定义一个定长的数组,相当于 C 语言中的 <code>char ans = {0};</code> 。dup 是 duplicate 的简写,表示重复。我们可以通过这种方法来取得内存空间存放数据。</p>
</li>
<li>
<p>汇编语言将所有的变量定义放在一起,即 <strong>data segment</strong> 区域中。</p>
</li>
<li>
<p>类似地,我们可以用 dw(define word) 定义字型数据(16位)。</p>
</li>
</ul>
</li>
<li>
<p><strong>注释</strong>&nbsp;</p>
<ul>
<li>如第 4 行,汇编语言源代码中,可以用分号 ; 表示本行中后面的内容均为注释。这与 C 中的 // 类似。</li>
</ul>
</li>
<li>
<p><strong>寄存器</strong></p>
<ul>
<li>
<p>CPU 本身只负责运算,不负责存储数据。数据一般存放在存储器中,CPU 需要使用数据时就会去存储器中调用数据。然而,CPU 的运算速度远高于内存的读写速度,因此为了提高效率,CPU 自带缓存和 <strong>寄存器</strong>&nbsp;(register)。缓存可以看做读写速度较快的内存,而寄存器是 "fastest, smallest and most expensive" 的,用来存储最常用的数据。</p>
</li>
<li>
<p>寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称。<br>8086CPU 有 14 个寄存器,分别为 AX, BX, CX, DX, SI, DI, SP, BP, IP, CS, SS, DS, ES, PSW,它们都是 16 位的。</p>
</li>
<li>
<p>本文中,我们用加括号的寄存器名称来表示寄存器中存储的数据。例如, (ax) 表示 ax 寄存器中存储的数据。<br></p>
</li>
<li>
<p><strong>AX, BX, CX, DX</strong> 这 4 个寄存器通常用来存放一般性数据,称为 <strong>通用寄存器</strong>&nbsp;。为了与上一代 CPU 兼容,每个通用寄存器都可以拆成两个 8 位寄存器独立使用,如 AX 可拆分为 AH 和 AL,BX 拆分为 BH 和 BL 等。H 和 L 分别表示高 8 位和低 8 位。<br>计算机存储信息的基本单位是一个 <strong>二进制位(bit)</strong>,一位可存储一个二进制数 0 或 1。每 8 位组成一个 <strong>字节(Byte)</strong>。每两个字节组成一个 <strong>字(word)</strong>,这两个字节分别称为高位字节和低位字节。<br></p>
</li>
<li>
<p>代码段寄存器&nbsp;<strong>CS</strong>(code segment) 和指令指针寄存器 <strong>IP</strong>(Instruction Pointer) 是 8086CPU 中最关键的两个寄存器。它们分别用来提供当前指令的段地址和偏移地址。即任意时刻,8086CPU 将 CS:IP 指向的内容当做命令执行。每条指令进入指令缓冲器后、执行前,IP += 所读取指令的长度,从而指向下一条指令。</p>
</li>
<li>
<p>其余寄存器将在用到时再做记录。</p>
</li>
</ul>
</li>
<li>
<p><strong>伪指令 assume</strong></p>
<ul>
<li>第 6 行 <code>assume cs:code, ds:data</code> 将段寄存器和段名建立了关系。即 assume 使得段寄存器储存了对应段的段地址。</li>
</ul>
</li>
<li>
<p><strong>标号</strong>&nbsp;</p>
<ul>
<li>第 7 行 <code>main:</code> 是一个标号。标号在程序中的主要用途是方便跳转语句的执行。跳转语句将在后面再做学习。</li>
</ul>
</li>
<li>
<p><strong>传送指令 mov</strong>&nbsp;</p>
<ul>
<li>传送指令 mov 的一般格式为 <strong>mov A, B</strong> ,用于将 B 的内容赋给 A(如果合法)。</li>
<li>传送指令在本文文末专门记录。</li>
</ul>
</li>
<li>
<p><strong>offset</strong>&nbsp;</p>
<ul>
<li>操作符 offset 的功能是取得标号的偏移地址。第 11 行 <code>mov dx, offset abc</code>&nbsp; 的作用就是将 abc 的偏移地址赋给 dx。&nbsp;</li>
</ul>
</li>
<li>
<p><strong>中断</strong>&nbsp;</p>
<ul>
<li>第 12 和 14 行的 <code>int 21h</code> 调用了中断。中断在本文文末专门记录。</li>
<li>在这里,<code>mov ah, 9</code>;<code>mov dx, offset abc</code>;<code>int 21h</code> 调用了中断 21h 的 09h 号功能,实现了对字符串 abc 的输出。中断 21h 的 09h 号功能实现的是:将自 ds:dx 开始、到 '$' 为止的字符串输出到标准输出设备上。</li>
<li><strong>程序返回</strong>:每个可执行文件的类型都来自于某一个正在运行的程序的调用。可执行文件运行完毕后,它要将 CPU 的控制权交还给调用它的程序,这个过程称为程序返回。 <code>mov ah, 4Ch</code>&nbsp; &nbsp;<code>int 21h</code>&nbsp;完成的就是这个过程。</li>
</ul>
</li>
<li>
<p><strong>end</strong>&nbsp;</p>
<ul>
<li>end 指令用于通知编译器:程序运行结束了。</li>
<li>在 end 后面加上一个标号,如 end main,则在起到上述效果的同时还会通知编译器程序的入口在什么地方(即偏移地址)。即,程序的入口由 end 指出。在本代码中,程序自标号 main 开始运行。</li>
</ul>
</li>
</ul>
<h2 id="mov-指令传送指令">mov 指令(传送指令)</h2>
<h3 id="将数据直接送入寄存器">将数据直接送入寄存器</h3>
<p>指令 <code>mov ax, 4E20h</code>&nbsp;表示将 4E20h 送入寄存器 AX。等价于高级语言中的 <code>AX = 4E20h;</code>&nbsp;(此后文中会大量使用高级语言的语法描述汇编指令)。<br></p>
<h3 id="将一个寄存器中的内容送入另一个寄存器">将一个寄存器中的内容送入另一个寄存器</h3>
<p>类似地, <code>mov ax, bx</code>&nbsp;表示 <code>AX = BX;</code>&nbsp;。<br></p>
<h3 id="将一个内存单元中的内容送入一个寄存器">将一个内存单元中的内容送入一个寄存器</h3>
<p>之前我们提到,8086CPU 中的地址由段地址和偏移地址组成。8086CPU 中有一个 <strong>DS</strong> 寄存器(段寄存器),用来存放要访问数据的段地址。<br>例如,我们要读取 10000h 单元的内容,可以用以下的程序段进行:</p>
<pre><code>mov bx, 1000h
mov ds, bx
mov al,
</code></pre>
<p>上面的三条指令将 10000h (1000:0) 中的数据读到了 al 中。可见,我们可以通过 <strong>mov</strong> <em><strong>register</strong></em>, <strong>[</strong> <em><strong>address</strong></em> <strong>]</strong> 的方式来将内存中 DS:address 的数据读到合法的寄存器 register 中。<br>值得注意的是,我们通过 1,2 两行将 1000h 放入了 DS,这是因为 8086CPU 不支持将数据直接送入段寄存器(ds, ss, cs, es)的操作。<br><br>
<br>我们还可以显式地规定我们调用的内存地址的段地址,如我们可以用 ds: 来表示我们调用的内存单元为 ds:0。这允许了我们使用 ds 以外的段地址。这样的 "ds:" "cs:" 等被称为 <strong>段前缀</strong>。逻辑地址中的偏移地址可以用常数表示,但是段地址必须用段寄存器表示。<br><br>
<br>另外,我们可以通过 表示内存单元 ds:bx,即段地址由 ds 提供,偏移地址由 bx 提供。<br>需要注意的是,8086CPU 中只有 <strong>bx, si, di, bp</strong> 这四个寄存器可以用来在 [] 中进行内存单元的寻址,其他寄存器进行这样的操作都是非法的。<br>同时,在 [] 中,这四个寄存器只能单独出现或以 bx, si / bx, di / bp, si / bp, di 的组合出现,如 是合法的,而 就是非法的。两个寄存器只能相加,不能相减<br>只要在 [] 中用到寄存器 bp,而指令中没有显式给出段地址,那么段地址就默认在 ss 中而不是 ds 中。<br><br>
<br>由于 8086CPU 是 16 位结构,因此可以一次性传送 16 位数据。比如:</p>
<pre><code>内存情况:
10000H 11
10001H 22

指令:
mov ax, 1000h
mov ds, ax
mov ax,

结果:
ax = 2211H
</code></pre>
<p>这是因为,我们将 1000:0 处存放的字数据(由两个字节组成)送入 ax 时,1000:0 处存放的是字数据的低 8 位,即 11;1000:1 处存放的数字数据的高 8 位,即 22。(小端规则:对于 8 位以上的变量,先存放低位,再存放高位,即低位的内存地址低于高位的内存地址。)执行 mov 时,字数据的低 8 位送入 al,高 8 位送入 ah,因此 ax = 2211H。<br><br>
<br>这也说明,mov 操作的内存单元的长度由其他操作对象(寄存器)指出。但是,两个不同长度的寄存器之间的传递是非法的,如 al 和 bx。<br><br>
<br>另外,下面的代码反映了一种常见的错误:</p>
<pre><code>data segment
xyz dw 1234h, 0ABCDh
data ends
...... ;略去
   
code segment
...... ;略去
mov ax, xyz
...... ;略去
</code></pre>
<p>在 C 语言的理解中,<code>short int xyz = {1234h, 0ABCDh};</code> 定义出的数组,xyz 的值应该为 0ABCDh。而实际上在汇编语言中,xyz(即 )指向的就是 xyz 物理地址 +1 的地址。假设 xyz 地址为 10000H,那么内存情况为(小端规则):</p>
<pre><code>10000h 10001h 10002h 10003h
34   12   CD   AB
</code></pre>
<p>实际上 xyz + 1 即 10001h,那么实际上程序认为 <code>mov ax, xyz</code> 调用了以 10001h 为低八位的 16 个字节,根据小端规则,ax 被赋值为 0CD12h。<br>因此,如果希望引用 0ABCDh,实际上要写的是&nbsp;<code>mov ax, xyz</code>&nbsp;。这是需要特别注意的。<br><br>
<br></p>
<h2 id="中断">中断</h2><br><br>
来源:https://www.cnblogs.com/xianyuxuan/p/12702512.html
頁: [1]
查看完整版本: 汇编语言入门学习 | 2 - 汇编语言代码基本结构