币圈巴菲特 發表於 2021-4-6 17:11:00

通过汇编语言了解程序的实际构成

<h4 id="问题">问题</h4>
<ol>
<li>
<p>本地代码的指令中,表示其功能的英语缩写称为什么?</p>
<p>助记符</p>
</li>
<li>
<p>汇编语言的源代码转换成本地代码的方式称为什么?</p>
<p>汇编</p>
</li>
<li>
<p>本地代码转换成汇编语言的源代码的方式称为什么?</p>
<p>反汇编</p>
</li>
<li>
<p>汇编语言的源文件的扩展名,通常是什么格式?</p>
<p>.asm</p>
</li>
<li>
<p>汇编语言程序中的段定义指的是什么?</p>
<p>构成程序的命令和数据的集合组</p>
</li>
<li>
<p>汇编语言的跳转指令,是在何种情况下使用的?</p>
<p>将程序流程跳转到其他地址时需要用到跳转指令。在汇编语言中,跳转指令可以实现循环和条件分支。</p>
</li>
</ol>
<h4 id="汇编语言和本地代码是一一对应的">汇编语言和本地代码是一一对应的</h4>
<p>表示其功能的英语缩写单词称为助记符,使用助记符的编程语言成为汇编语言。汇编语言编写的源代码最终也要转换成本地代码才能运行,负责转换工作的程序称为汇编器,转换这一处理本身称为汇编。用汇编语言编写的源代码和本地代码是一一对应的。因而,本地代码也可以反过来转换成汇编语言的源代码,这一过程称为反汇编,持有该功能的逆变换程序称为反汇编程序。</p>
<p>不过,本地代码变换成高级语言源代码的反编译,则要比反汇编要困难许多,几乎是不太可能的,因为高级语言的源代码和本地代码不是一一对应的。</p>
<h4 id="通过编译器输出汇编语言的源代码">通过编译器输出汇编语言的源代码</h4>
<p>大部分C语言编译器,都可以把利用C语言编写的源代码转换成汇编语言的源代码,而不是本地代码。利用该功能,就可以对C语言的源代码和汇编语言的源代码进行比较研究。</p>
<p>汇编语言源文件的扩展名通常用.asm来表示。</p>
<h4 id="不会转换成本地代码的伪指令">不会转换成本地代码的伪指令</h4>
<p>汇编语言的源代码,是由转换成本地代码的指令和针对汇编器的伪指令构成的。伪指令负责把程序的构造即汇编的方法只是给汇编器。不过伪指令本身是无法汇编转换成本地代码的。</p>
<p>由伪指令segment和ends围起来的部分,是给构成程序的命令和数据的集合体加上一个名字而得到的,称为段定义。一个程序由多个段定义构成。</p>
<p>group这一伪指令,表示的是把多个段定义汇总成一个新名字的组。</p>
<p>伪指令proc和endp围起来的部分,表示的是过程procedure的范围。在汇编语言中,过程相当于C语言的函数的形式。</p>
<p>伪指令end表示的是源代码的结束。</p>
<h4 id="汇编语言的语法是操作码操作数">汇编语言的语法是操作码+操作数</h4>
<p>也存在只有操作码,没有操作数的指令</p>
<p>操作码表示的是指令动作,操作数表示的是指令对象。</p>
<p>存在多个操作数时,用逗号分隔。</p>
<p>能够使用何种形式的操作码,是由CPU的种类决定的。</p>
<p>常用的操作码功能:</p>
<table>
<thead>
<tr>
<th>操作码</th>
<th>操作数</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr>
<td>mov</td>
<td>A, B</td>
<td>把B的值赋给A</td>
</tr>
<tr>
<td>and</td>
<td>A, B</td>
<td>把A同B的值相加,并将结果赋给A</td>
</tr>
<tr>
<td>push</td>
<td>A</td>
<td>把A的值存储在栈中</td>
</tr>
<tr>
<td>pop</td>
<td>A</td>
<td>从栈中读取出值,并将其赋给A</td>
</tr>
<tr>
<td>call</td>
<td>A</td>
<td>调用函数A</td>
</tr>
<tr>
<td>ret</td>
<td>无</td>
<td>将处理返回到函数的调用源</td>
</tr>
</tbody>
</table>
<p>寄存器是CPU中的存储区域。不过,寄存器不仅仅具有存储指令和数据的功能,也有运算功能。</p>
<p>主要寄存器:</p>
<table>
<thead>
<tr>
<th>寄存器名</th>
<th>名称</th>
<th>主要功能</th>
</tr>
</thead>
<tbody>
<tr>
<td>eax</td>
<td>累加寄存器</td>
<td>运算</td>
</tr>
<tr>
<td>ebx</td>
<td>基址寄存器</td>
<td>存储内存地址</td>
</tr>
<tr>
<td>ecx</td>
<td>计数寄存器</td>
<td>计算循环次数</td>
</tr>
<tr>
<td>edx</td>
<td>数据计数器</td>
<td>存储数据</td>
</tr>
<tr>
<td>esi</td>
<td>源基址寄存器</td>
<td>存储数据发送源的内存地址</td>
</tr>
<tr>
<td>edi</td>
<td>目标基址寄存器</td>
<td>存储数据发送目标的内存地址</td>
</tr>
<tr>
<td>ebp</td>
<td>扩展基址指针寄存器</td>
<td>存储数据存储领域基点的内存地址</td>
</tr>
<tr>
<td>esp</td>
<td>扩展栈指针寄存器</td>
<td>存储栈中最高位数据的内存地址</td>
</tr>
</tbody>
</table>
<h4 id="最常用的mov指令">最常用的mov指令</h4>
<p>操作数可以指定寄存器、常数、标签,以及方括号围起来的内容。如果指令了没有用方括号围起来的内容,就表示对该值进行处理;如果指令了用方括号围起来的内容,方括号中的值则会被解释为内存地址,然后就会对该内存地址对应的值进行读写操作。</p>
<p>dword ptr(double word pointer)表示的是从指定内存地址读出4字节的数据。</p>
<h4 id="对栈进行push和pop">对栈进行push和pop</h4>
<p>栈是存储临时数据的区域,它的特点是通过push指令和pop指令进行数据的存储和读出,称为入栈和出栈。push和pop指令都只有一个操作数,这是因为,对栈进行读写的内存地址是由esp寄存器进行管理的。我是怕寄存器的值会自动进行更新,因而程序员就没有必要指定内存地址了。</p>
<h4 id="函数调用机制">函数调用机制</h4>
<p>形参中位于后面的数值先入栈,这是C语言的规定。</p>
<p>在汇编语言中,函数名表示的是函数所在的内存地址。</p>
<p>虽然通过两次pop指令也可以实现湛清里,不过采用esp寄存器加8的方式会更有效率。</p>
<p>push和pop必须以4字节为单位对数据进行入栈和出栈处理,长度小于4字节的数据也占用了4字节的栈区域。</p>
<p>编译器有最优化功能。最优化功能是编译器在本地代码上费劲功夫实现的,其目的是让编译后的程序运行速度更快、文件更小。</p>
<h4 id="函数内部的处理">函数内部的处理</h4>
<p>CPU拥有的寄存器是由数量限制的。在函数调用前,调用源有可能已经在使用寄存器了。因而,在函数内部利用的寄存器,要尽量返回到函数调用前的状态。为此,我们就需要将其暂时保存在栈中,然后 再在函数处理完毕之前出栈,使其返回到原来的状态。</p>
<p>在C语言中,函数的返回值必须通过eax寄存器返回,这也是规定。不过,eax寄存器无需还原到原始状态。</p>
<p>函数的参数是通过栈来传递的,返回值是通过寄存器来返回的。</p>
<h4 id="始终确保全局变量用的内存空间">始终确保全局变量用的内存空间</h4>
<p>C语言中,在函数外部定义的变量称为全局变量,在函数内部定义的变量称为局部变量。</p>
<p>初始化的全局变量会被汇总到名为_DATA的段定义中;没有初始化的全局变量会被汇总到名为_BSS的段定义中;指令会被汇总到名为_TEXT的段定义中。</p>
<p>标签表示的是相对于段定义起始位置的位置。编译后的函数名和变量名会附加一个下划线,这也是Borland C++的规定。</p>
<p>dd(define double word)表示的是由两个长度为2的字节领域(word),也就是4字节的意思。</p>
<p>db(define byte)表示有1个长度是1字节的内存空间,db 4 dup(?)就是4字节的内存空间,但值尚未确定。</p>
<h4 id="临时确保局部变量用的内存空间">临时确保局部变量用的内存空间</h4>
<p>局部案例是临时保存在寄存器和栈中的。函数内部利用的栈,在函数处理完毕后会恢复到初始状态,因此局部变量的值也就被销毁了,而寄存器也可能会被用于其他目的。</p>
<p>寄存器先时就使用寄存器,寄存器空间不足时就使用栈。局部变量利用寄存器,是Borland C++编译器最优化的运行结果。旧的编译器没有类似的最优化功能,局部变量就可能会仅仅使用栈。至于使用哪个寄存器则要由编译器来决定。这种情况下,寄存器只是被单纯地用于存储变量的值,和其本身的角色没有任何关系。</p>
<h4 id="循环处理条件分支的实现方法">循环处理、条件分支的实现方法</h4>
<p>C语言的for语句是通过在括号中指定循环计数器的初始值、循环的继续条件、循环计数器的更新来进行循环处理的。与此相对,在汇编语言的源代码汇总,循环是通过比较指令cmp和跳转指令jl来实现的。</p>
<p>与mov指令相比,xor指令的处理速度更快。</p>
<p>汇编语言中比较指令的结果,会存储在CPU的标志寄存器中。不过,标志寄存器的值,程序是无法直接参考的。实际上,汇编语言有多个跳转指令,这些跳转指令会根据标志寄存器的值来判定是否需要跳转。</p>
<p>虽然大部分的C语言参考书中都写着“为了便于理解程序的结构,应尽量避免使用无条件分支的goto语句”,不过,在汇编语言这一领域中,如果不使用相当于goto语句的jmp指令,就无法实现循环和条件分支。</p>
<h4 id="了解程序运行方式的必要性">了解程序运行方式的必要性</h4>
<p>多线程处理的Bug,如果没有调查过汇编语言的源代码,即对程序的实际运行方式不了解的话,是很难找到其原因的。</p>
<p>为了避免这种Bug,我们可以采用以函数或C语言源代码的行为单位来禁止线程切换的锁定方法。通过锁定,在特定范围内的处理完成之前,处理不会被切换到其他函数中。</p>
<p>现在基本上没有人用汇编语言来编写程序了。因为C语言等高级编程语言用1行就可以完成的事,汇编语言需要多行,效率很低。不过,汇编语言的经验还是很重要的。因为借助汇编语言,我们可以更好地了解计算机的机制。特别是对专业程序员来说,至少要有一次使用汇编语言的经验。</p><br><br>
来源:https://www.cnblogs.com/fr-ruiyang/p/14622734.html
頁: [1]
查看完整版本: 通过汇编语言了解程序的实际构成