汇编语言简易教程(7):初探指令集
<h1 id="汇编语言简易教程7初探指令集">汇编语言简易教程(7):初探指令集</h1><blockquote>
<h1 id="组织顺序">组织顺序</h1>
<p>将按照以下顺序进行简单的介绍:</p>
<ol>
<li>移动</li>
<li>转换</li>
<li>数值</li>
<li>逻辑</li>
<li>控制</li>
</ol>
<h1 id="符号约定">符号约定</h1>
<table>
<thead>
<tr>
<th>操作符号</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code><reg></code></td>
<td>寄存器操作数, 该操作数必须为寄存器</td>
</tr>
<tr>
<td><code><reg8>, <reg16>,<reg32>, <reg64></code></td>
<td>带有指定大小的寄存器操作数, 例如<code>reg8</code>指的是byte大小的寄存器, 例如al, bl.<br><code>reg32</code>指的是double-word(双字, 32bit)大小的寄存器, 例如<code>eax, ebx</code></td>
</tr>
<tr>
<td><code><dest></code></td>
<td>目标操作数, 该操作数必须为一个寄存器或者是内存. 需要注意的是,作为操作数被使用, 其内容会被覆写.</td>
</tr>
<tr>
<td><code><RXdest></code></td>
<td>浮点目标寄存器操作数。操作数必须是浮点寄存器。需要注意的是,作为操作数被使用, 其内容会被覆写.<br></td>
</tr>
<tr>
<td><code><src></code></td>
<td>源操作数, 通常源操作数在操作前后不会改变.</td>
</tr>
<tr>
<td><code><imm></code></td>
<td>立即数, 一个使用十进制/ 16进制/ 八进制/ 二进制描述的值.</td>
</tr>
<tr>
<td><code><mem></code></td>
<td>内存位置, 可以是变量名或间接引用(即内存地址)</td>
</tr>
<tr>
<td><code><op> or <operand></code></td>
<td>操作数、寄存器或内存.</td>
</tr>
<tr>
<td><code><op8>, <op16>,<op32>, <op64></code></td>
<td>操作数,寄存器或内存,有特定的大小要求。例如,op8 仅表示字节大小的操作数,而 reg32 仅表示双字大小的操作数。</td>
</tr>
<tr>
<td><code><label></code></td>
<td>代码标签, 可能用作跳转/ 标识等用途.</td>
</tr>
</tbody>
</table>
<p>通常来说, 立即数使用10进制描述, 这是出于可读性的考虑, 当然也可以在前面加上 <code>0x</code>表示Hex编码(16进制)</p>
<h1 id="移动指令">移动指令</h1>
<p>通常,数据必须从 RAM 移入 CPU 寄存器才能进行操作。</p>
<p>一旦计算完成,结果可以从寄存器复制并放入变量中。</p>
<p>示例程序中有许多简单的公式可以执行这些步骤。这个基本的数据移动操作是用移动指令执行的</p>
<h2 id="表达式">表达式</h2>
<p><code>mov <dest>, <src></code></p>
<p>源操作数从源操作数复制到目标操作数。</p>
<p>源操作数的值不变。目标和源操作数的大小必须相同(都是字节、都是字等)。</p>
<p>目标操作数不能是立即数。两个操作数都不能是内存。如果需要内存到内存的操作,则必须使用两条指令。</p>
<h2 id="示意图">示意图</h2>
<p><img src="assets/image-20240410121934-vtcgq7w.png"></p>
<p>需要注意:当目标寄存器操作数为双字大小并且源操作数为双字大小时,四字寄存器的高位双字被设置为零。仅当目标操作数是双字大小的整数寄存器时才适用。</p>
<h2 id="示例">示例</h2>
<pre><code class="language-ass">mov eax, 100 ; eax = 0x00000064
mov rcx, -1; rcx = 0xffffffffffffffff
mov ecx, eax ; ecx = 0x00000064
</code></pre>
<p>More About Mov</p>
<h2 id="地址和值">地址和值</h2>
<p>访问内存的唯一方法是使用方括号([])。省略括号将不会访问内存,而是获取项目的地址。例如:</p>
<pre><code class="language-ass">mov rax, qword ; value of var1 in rax
mov rax, var1 ; address of var1 in rax
</code></pre>
<p><span style="font-weight: bold" class="bold">省略括号不是错误,因此汇编器不会生成错误消息或警告。但是者会导致你的程序实际运行时产生歧义</span></p>
<p>除此之外, 可以使用<code>lea</code>, 需要注意lea其实是一个非常强大的指令, 可以参考: LEA</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013711634-765635751.png"></p>
<h1 id="转换">转换</h1>
<p>有时需要从一种尺寸转换为另一种尺寸。例如,对于公式中的某些计算,可能需要将字节转换为双字。用于转换的过程取决于操作数的大小和类型。以下部分总结了如何执行转换。</p>
<h2 id="缩小转换">缩小转换</h2>
<p>缩小转换是从较大类型转换为较小类型(即字到字节或双字到字)</p>
<p>缩小转换范围不需要特殊说明。可以直接访问存储器位置或寄存器的下部部分。</p>
<p>例如,如果将50(0x32)的值放入rax寄存器中,则可以直接访问al寄存器来获取该值,如下所示:</p>
<pre><code class="language-ass">mov rax, 50
mov byte , al
</code></pre>
<p>这个例子是合理的,因为 50 的值适合一个字节值。</p>
<p>然而,如果将500(0x1f4)的值放入rax寄存器中,al寄存器仍然可以被访问</p>
<pre><code class="language-ass">mov rax, 500
mov byte , al
</code></pre>
<p>在此示例中,bVal 变量将包含 0xf4,这可能会导致不正确的结果。</p>
<p>程序员有责任确保正确执行缩小转换。与编译器不同,不会生成警告或错误消息</p>
<h2 id="扩大转换">扩大转换</h2>
<p>扩大转换是从较小的类型到较大的类型(例如,字节到字或字到双字)。</p>
<p>由于大小正在扩展,因此必须根据原始值的符号设置高位。因此,必须知道数据类型(有符号或无符号),并且必须使用适当的过程或指令</p>
<h3 id="无符号转换">无符号转换</h3>
<p>对于无符号加宽转换,内存位置或寄存器的上部必须设置为零。</p>
<p>由于无符号值只能为正,因此高位只能为零。例如,要将al寄存器中的字节值50转换为rbx中的四字值,可以执行以下操作:</p>
<pre><code class="language-ass">mov al, 50
mov rbx, 0
mov bl, al
</code></pre>
<p>由于 rbx 寄存器被设置为 0,然后低 8 位被设置为 al 中的值(本例中为 50),因此整个 64 位 rbx 寄存器现在为 50。</p>
<p>这个一般过程可以在内存或其他上执行寄存器。程序员有责任确保这些值适合所使用的数据大小</p>
<p>也可以使用特殊的移动指令执行从较小尺寸到较大尺寸的无符号转换,如下所示</p>
<p><code>movzx <dest>, <src></code></p>
<p>这将用0填充高位。</p>
<p>movzx 指令不允许四字目标操作数与双字源操作数。</p>
<p>如前所述,具有双字寄存器目标操作数和双字源操作数的 mov 指令会将四字目标寄存器的高位双字清零。</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013713021-2023677983.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013713832-1774523695.png"></p>
<h3 id="有符号转换">有符号转换</h3>
<p>这部分需要联系有符号整数</p>
<p>对于有符号加宽转换,高位必须设置为 0 或 1,具体取决于原始值是正值还是负值。</p>
<p>这是通过符号扩展操作来执行的。具体来说,原始值的高位指示该值是正(0)还是负(1)。原始值的高位被扩展为新的、加宽的值的高位。</p>
<p>例如,假定 ax 寄存器设置为 -7 (0xfff9),则这些位将设置如下</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013714538-1397243405.png"></p>
<p>由于该值为负,因此高位(位 15)为 1。为了将 ax 寄存器中的字值转换为 eax 寄存器中的双字值,需要扩展高位(本例中为 1)或复制到整个高位字(位 31-16),结果如下</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013715442-1307314067.png"></p>
<p>参考指令:</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013716153-1967544327.png"></p>
<p>它将对源参数执行符号扩展操作。</p>
<p>movsxd 指令是通用形式,movsxd 指令用于允许四字目标操作数与双字源操作数</p>
<p>执行有符号加宽转换的指令摘要如下:</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013716724-604452056.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013717335-535939736.png"></p>
<h1 id="整数数值计算">整数数值计算</h1>
<h2 id="addition加">Addition(加)</h2>
<p>一般形式: <code>add <dest>, <src></code></p>
<p>表示含义: <code><dest> = <dest> + <src></code></p>
<p>注意:</p>
<ol>
<li>目标操作数和源操作数必须具有相同的大小(均为字节、均为字等)。</li>
<li>目标操作数不能是立即数。</li>
<li>两者不能都是内存。</li>
<li>如果需要内存到内存的加法操作,必须使用两条指令</li>
</ol>
<p>除了基本的加法指令之外,还有一条自增指令,可将加法到指定的操作数。自增指令的一般形式如下:</p>
<p><code>inc <operand></code> 含义(<code><operand> = <operand> + 1</code>)</p>
<p>结果与使用 add 指令(并加一)完全相同。使用内存操作数时,需要显式类型规范(例如,字节、字、双字、q字)来明确定义大小</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013717982-116802685.png"></p>
<h3 id="带有进位的加">带有进位的加</h3>
<p>带进位的加法是一种特殊的加法指令,它将包含来自先前加法操作的进位。</p>
<p>当添加非常大的数字时,特别是大于机器寄存器大小的数字时,这非常有用</p>
<p>对于汇编语言程序,最低有效四字(LSQ)与加法指令相加,然后立即最高有效四字(MSQ)与 adc 相加,这将添加四字并包括来自先前加法操作的进位.</p>
<p><code>adc <dest>, <src></code> 表示含义: <code><dest> = <dest> ++ <carrybit></code></p>
<p>具体来说,将源操作数和目标操作数以及进位相加,并将结果放在目标操作数中(覆盖以前的值)。</p>
<p>进位是 rFlag 寄存器的一部分。 源操作数的值保持不变。</p>
<p>目标操作数和源操作数的大小必须相同(两个字节、两个字等)。</p>
<p>目标操作数不能是即时操作数。 两个操作数,都不能被记住。</p>
<p>如果需要内存到内存的添加操作,则必须使用两条指令</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013718594-1797241164.png"></p>
<h2 id="subtraction减">Subtraction(减)</h2>
<p>一般形式: <code>sub <dest>, <src></code></p>
<p>表示含义: <code><dest> = <dest> - <src></code></p>
<p>一个特殊的自减操作:<code>dec <operand></code></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013719141-1907427678.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013719774-1404641658.png"></p>
<h2 id="integer-multiplication整数乘法">Integer Multiplication(整数乘法)</h2>
<h3 id="无符号乘法">无符号乘法</h3>
<p><code>mul <src></code></p>
<p>其中源操作数必须是寄存器或内存位置。 不允许使用直接操作数.</p>
<p>对于单操作数乘法指令,必须使用 A 寄存器 (al/ax/eax/rax)对于其中一个操作数(AL 表示 8 位,ax 表示 16 位,eax 表示 32 位,rax 表示 64 位)。</p>
<p>另一个操作数可以是内存位置或寄存器,但不能是直接操作数。</p>
<p>此外,结果将根据乘以的大小放置在 A 寄存器和可能的 D 寄存器中。 下表显示了字节、字、双字和四字无符号乘法的各种选项</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013720385-1703541699.png"></p>
<p>如图所示,在大多数情况下,整数乘法使用 A 和 D 寄存器的组合。 这可能非常令人困惑。例如,当将 rax(64 位)乘以四字操作数(64 位)时,乘法指令将提供双四字结果(128 位)。</p>
<p>在处理非常大的数字时,这可能是有用和重要的。 由于 64 位体系结构只有 64 位寄存器,因此 128 位结果必须放置在两个不同的四字(64 位)寄存器中,rdx 表示高阶结果,rax 表示低阶结果,通常写为 rdx:rax(按照惯例)。</p>
<p>但是,这种使用两个寄存器也适用于较小的尺寸。</p>
<p>例如,将 ax(16 位)乘以字操作数(也是 16 位)的结果提供双字(32 位)结果。 但是,结果不是放在 eax 中(这可能更容易),而是放在两个寄存器中,dx 表示高阶结果(16 位),ax 表示低阶结果(16 位),通常写为 dx:ax(按照惯例)。 由于双字(32 位)结果位于两个不同的寄存器中,因此可能需要两次移动才能保存结果。这种寄存器配对(即使不需要)也是由于对以前早期版本的体系结构的旧支持。 虽然这有助于确保向后兼容性,但可能会非常令人困惑。</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013720972-1398360218.png"></p>
<h3 id="有符号数乘法">有符号数乘法</h3>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013721847-569145114.png"></p>
<p>当使用单个操作数乘法指令时,imul 的布局与 mul 相同(如前所述)。 但是,操作数仅被解释为有符号</p>
<p>当使用两个操作数时,目标操作数和源操作数相乘,并将结果放在目标操作数中(覆盖前一个值)</p>
<p><code><dest> = <dest> * <src/imm></code></p>
<p>对于两个操作数,<src/imm>操作数可以是寄存器、内存位置或即时值。 即时值的大小限制为源操作数的大小,最多为双字大小(32 位),即使对于四字(64 位)乘法也是如此。最终结果被截断为目标操作数的大小。 不支持字节大小的目标操作数</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013722439-1636387201.png"></p>
<h2 id="integer-division整数除法">Integer Division(整数除法)</h2>
<p>除法指令除以两个整数操作数。 在数学上,有处理有符号值除法的特殊规则。 因此,无符号除法 (div) 和有符号除法 (idiv) 使用不同的指令</p>
<p>除法要求被除数必须大于除数。 为了除以 8 位除数,被除数必须为 16 位(即较大的大小)。 同样,16 位除数需要 32 位被除数。 而且,32 位除数需要 64 位除数。与乘法一样,在大多数情况下,整数除法使用 A 和 D 寄存器的组合。 这种寄存器配对是由于对体系结构的早期版本的旧版支持。 虽然这有助于确保向后兼容性,但可能会造成相当混乱。此外,A,可能还有 D 寄存器,必须组合用于除法.</p>
<p>对被除数使用较大尺寸的操作数与单个操作数乘法相匹配。</p>
<p>对于简单的除法,可能需要进行适当的转换,以确保正确的进行除法。对于无符号除法,被除数的高位部分可以设置为零。</p>
<p>对于有符号除法,可以使用适用的转换指令来设置被除数的高位部分。与往常一样,除以零将使程序崩溃并损坏时空连续体。所以,尽量不要除以零.</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013723056-578120745.png"></p>
<p>一般形式:</p>
<p><code>div <src> ; unsigned division</code></p>
<p><code>idiv <src> ; signed division</code></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013723764-647403567.png"></p>
<h2 id="逻辑指令">逻辑指令</h2>
<h3 id="逻辑运算指令">逻辑运算指令</h3>
<p>这里的逻辑运算指的是 逻辑数学的运算: and, or, not, xor</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013724432-1351836217.png"></p>
<p><span style="font-weight: bold" class="bold">指令介绍</span></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013725043-494611495.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013725646-229915117.png"></p>
<h3 id="移位运算">移位运算</h3>
<p>移位运算将操作数内的位左移或右移。</p>
<p>移位位的两个典型原因包括为了某种特定目的或可能为了执行乘法或除以 2 的幂而隔离操作数内的位子集。所有位都移位一位。移出操作数的位会丢失,并在另一侧添加一个 0 位.</p>
<h4 id="逻辑偏移">逻辑偏移</h4>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013726249-676100989.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013726766-139489541.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013727439-1508258716.png"></p>
<h4 id="算术偏移">算术偏移</h4>
<p>算术右移也是一种按位操作,它将源寄存器的所有位移动指定的位数,并将结果放入目标寄存器。</p>
<p>源操作数中的每一位都被移动指定的位数,新空出的位位位置被填充。对于算术左移,原始最左边的位(符号位)被复制以填充所有空位。这称为符号扩展。</p>
<p>需要注意, 在左移的时候, 是会直接抛弃掉符号位的.</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013728022-2125435736.png"></p>
<p>但是在右移的时候, 会保留符号位.</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013728524-1821542348.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013729581-18952606.png"></p>
<h4 id="旋转操作">旋转操作</h4>
<p>旋转操作将操作数内的位左移或右移,移出操作数外部的位将被旋转并放置在另一端。例如,如果字节操作数 100101102 被旋转到右侧 1 位,结果为 01001011。如果将字节操作数 100101102 左移 1 位,则结果为 00101101</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013730212-4747630.png"></p>
<h1 id="控制指令">控制指令</h1>
<h2 id="标签">标签</h2>
<p>程序标签是控制语句的目标或跳转到的位置。</p>
<p>例如,循环的开始可能用诸如“loopStart”之类的标签来标记。代码可以通过跳转到标签来重新执行</p>
<p>通常,标签以字母开头,后跟字母、数字或符号(仅限“<em>”),以冒号(“:”)结尾。可以使用非字母字符(即数字、“</em>”、“$”、“#”、“@”、“~”或“?”)开始标签。然而,这些通常传达特殊含义,通常程序员不应该使用。标签区分大小写。</p>
<h2 id="无条件控制指令">无条件控制指令</h2>
<p>无条件指令提供无条件跳转到程序中用程序标签表示的特定位置。</p>
<p>目标标签必须精确定义一次、可访问且位于原始跳转指令的范围内</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013730783-1461222404.png"></p>
<h2 id="条件控制指令">条件控制指令</h2>
<p>条件跳转指令将根据先前比较操作的结果跳转或不跳转到提供的标号。</p>
<p>比较指令将比较两个操作数并将比较结果存储在therFlag寄存器中。条件跳转指令将根据 rFlag 寄存器的内容进行操作(跳转或不跳转)。</p>
<p>这要求比较指令后面紧跟着条件跳转指令。如果在比较和条件跳转之间放置其他指令,则 rFlag 寄存器将被更改,并且条件跳转可能无法反映正确的条件.</p>
<p>条件控制指令必须配合能够配合 标志寄存器 (rFlags)才能实现, 通常用来IF语句的功能.</p>
<h4 id="形式">形式</h4>
<p><code>cmp <op1>, <op2></code></p>
<p>其中 <op1> 和 <op2> 不变并且大小必须相同。其中一个(但不能同时)可以是内存操作数。 <op1> 操作数不能是立即数,但 <op2> 操作数可以是立即数</op2></op1></op2></op1></p>
<h3 id="条件跳转指令">条件跳转指令</h3>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013731364-261559245.png"></p>
<h3 id="跳转越界">跳转越界</h3>
<p>条件跳转中直接跳转到标签称之为 短跳转, 目标标签距离被限制在128 byte以内.</p>
<p>但是通常通常来说<code>jmp</code>是没有距离限制的, 因此可以采用二级跳的方式来解决.</p>
<p>类似于:</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013732021-52639874.png"></p>
<h3 id="遍历循环">遍历/循环</h3>
<p>概述的基本控制指令提供了一种迭代或循环的方法。</p>
<p>基本循环可以由一个计数器组成,该计数器在循环的底部或顶部通过比较和条件跳转进行检查</p>
<pre><code class="language-c">int sum = 0;
int cnt = 1;
for int i = 1; i < 15; i++ {
sum += cnt;
cnt = cnt + 2;
}
</code></pre>
<p>让我们先回忆下经典的遍历形式, 以一个技术求和作为例子:</p>
<p>我们可以处理为几个部分:</p>
<ol>
<li>初始条件</li>
<li>结束条件</li>
<li>状态变化</li>
</ol>
<h4 id="只使用jne实现的遍历">只使用jne实现的遍历</h4>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013732584-2008219875.png"></p>
<p>这只是完成奇数求和任务的多种不同方法之一。在本例中,rcx 用作循环计数器,rax 用作当前奇数整数(适当初始化为 1 并递增 2)</p>
<h4 id="使用loop实现的遍历">使用loop实现的遍历</h4>
<h5 id="基本形式">基本形式</h5>
<p><code>loop <label></code></p>
<h5 id="含义">含义</h5>
<p>它将执行 rcx 寄存器的递减,与 0 比较,如果 rcx ≠ 0,则跳转到指定的标签。该标签必须恰好定义一次。</p>
<p>因此,循环指令提供与前面的三行代码相同的功能示例程序。以下几组代码是等效的</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013733107-287164909.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013733620-968454782.png"></p>
<p>因此, 我们可以将上述的循环进行改造.</p>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013734124-1810465054.png"></p>
<h4 id="指令总结">指令总结</h4>
<p><img src="https://img2023.cnblogs.com/blog/3407132/202404/3407132-20240411013734715-943108791.png"></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/pDJJq/p/18127896/simple-tutorial-of-assembly-language-7-early-exploration-instruction-set-z2pufnm
頁:
[1]