我是靠谱客的博主 寒冷枫叶,最近开发中收集的这篇文章主要介绍带你学习《深入理解计算机系统》程序语言的底层描述(1)——汇编基础概念的开始之入门,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

该主题默认在类unix操作系统中进行讨论

 

gcc编译器可通过不同的参数,生成不同阶段的编译文件,比如,我们想生成可执行文件,就用gcc -o;要生成目标代码.o,就用gcc -c;要生成汇编代码.s,就用gcc -S,

程序译的顺编序是,编译器先根据.c源文件产生.s汇编文件,然后汇编器会把.s转换成目标文件.o,最后由链接器将.o文件与unix库函数合并,产生可执行文件。

当我们得到.o时,还可以用objdump -d  *.o,对目标文件进行反汇编,屏幕会打印出十六进制与汇编文件的对应解释。

以上说的都是操作方法,接下来的几章我们将浅析汇编语言的内幕。

 

首先来看汇编语言的基础知识

一:数据格式

由于计算机经历从16位机扩展到32位机的过程,intel沿用的以前的标准,即将16位(即2字节)定义为“字”,32位定义为“双字”。而8位仍成为“字节”,用符号表示如下:

        字:w   16位

    双字:l     32位

    字节:b    8位

单精度:s    8位

双精度:l     32位

 

汇编语言在操作上沿用了这样的命名方式,比如数据传送命令movl,我们就能看出这是对32位4字节的操作

 

二:寄存器

汇编语言简单说就是游走于寄存器、存储器(内存)之间进行运算和操作的语言,因此寄存器的概念是至关重要的。

下面是整数寄存器示意图:

                                                        31                                        15                 8  7                  0

%eax                           %ax
%ah%al
%ecx                           %cx
%ch%cl
%edx                           %dx
%dh%dl
%ebx                           %bx
%bh%bl
%esi                           %si 
%edi                           %di 
%esp                         %sp栈指针
%ebp                         %bp帧指针

 

从示意图来看,八个寄存器中,每个寄存器都是32位,每个寄存器都有直接引用低16位的寄存器标识,比如%esi的低16位寄存器可以用%si来直接访问。同样的道理,%bh、%bl还可以直接访问%bx的高八位和低八位。

还有,%esp默认是栈指针,%ebp是帧指针,这些在后面还有论述。

 

三:操作数指示符

常见操作数指示符有$、Imm、(),s

  • $后面跟一个整数,表示“立即数”,类似C语言里的常量数,如我们要把一个5赋给一个寄存器, 在语句中用$5表示;
  • Imm称为立即数偏移,顾名思义,是在寻址时进行地址偏移的
  • ()类似间接寻址,类似C语言里的“*”
  • s伸缩因子,必须是1、2、4、8

通过下面这个例子方便理解:

(x86的这种复杂指令集操作真麻烦,像mips和arm等精简指令集的操作就要优美简洁得多,反正我更愿意阅读和写后者的汇编)

假设下面五个地址代表的存储器中分别存储这五个值:

 

(虚拟)内存地址
0x1000xFF
0x1040xAB
0x1080x13
0x10C0x11

 

以下是三个寄存器所存储的值:

 

寄存器
%eax0x100
%ecx0x1
%edx0x3

 

当汇编语句中引用如下操作数所表示的值、对应的寄存器、以及分析如下:

 

操作数注释备注
%eax 0x100 %eax调用寄存器,直接出寄存器内的值
0x1040xAB绝对地址0x104存储器的绝对地址访问,直接出地址内的值
$0x1080x108立即数相当于常量
(%eax)0xFF绝对地址0x100间接寻址,通过%eax引用地址0x100,得到值,相当于*(0x100)操作
4(%eax)0xAB绝对地址0x104间接寻址,%eax值偏置4引用地址0x104,得值,*(0x104)
9(%eax,%edx)0x11绝对地址0x10C间接寻址,%eax、%edx值相加,再偏置9:*(0x100+0x3+0x9=0x10C)
260(%ecx,%edx)0x13绝对地址0x108同上
0xFC(,%ecx,4)0xFF绝对地址0xFF间接寻址,%ecx值伸缩4倍(0x1*4),再偏置0xFC,实际访问的是地址0x100中的值
(%eax,%edx,4)0x11绝对地址0x10C两个例子的结合,*(0x100+0x3*4=0x10C)

 

倒数第二个例子比较有意思,很容易看成0xFC+4=0xFF,算数没过关吧,嚯嚯,正确的应该是0xFC+4=0x100,而地址0x100里面存的是0xFF,哇咔咔。

 

汇编语言指令基础

 

一:数据传送指令mov

注意,汇编语言的赋值语句顺序和C语言是相反的,也就说表达式右边的对象是被赋值的对象,接收新值。

mov实质上有两种操作,一种是值传送,比如movl  $4, %eax,意思是把4这个数值传送(覆盖)给寄存器eax,也可以mov %edx, %eax实现寄存器之间的数值传递

另一种是将对象理解成地址,并根据这个地址找到相应的存储器位置,读出里面的值并传送,即间接寻址 。还是以上面存储器中的值为例,执行movl:

 

movl  (%eax), %ecx    //  间接寻址,先获取寄存器eax里的值0x100,再识别成存储器0x100里的存值0xFF。再赋给寄存器ecx

movl  4(%eax), %ecx  //同上,先获取4+0x100=0x104的值,再到存储器地址0x104中取出0xAB,最后赋值给ecx

下面的例子是复杂的间接寻址,可以参照上面的表格分析事例:

movl  257(%ecx, %edx, 2) , %ecx  //  257 + 0x1 + 0x3*2 = 0x108,然后将0x108中的值0x13赋给寄存器ecx

 

通过上面两条语句,寄存器ecx里的值先赋成0xFF,再赋成0xAB,最后覆盖成0x13。

 

二:栈指针%esp和帧指针%ebp

指令pushl %ebp,是压栈,将%ebp里的值压入栈中,而栈指针是%esp,即栈顶,于是该语句等价于这样两条指令

subl  $4, %esp    //将栈指针的值减4——栈底是高地址,逐个往低地址扩展,所以要减4

movl %ebp, (%esp)    //将%ebp里的值,赋给栈指针指向的新地址所代表的存储空间

 

指令pop %ebp,是出栈,将%esp里的地址对应的空间中的值赋值给%ebp,等价于这样两条指令

movl (%esp), %ebp

addl  $4, %esp

 

帧指针的命名可以通过函数参数理解:

%ebp和%eax两个寄存器常被用于函数操作,比如将如下C代码对应到汇编代码:

int exchange(int *xp, int y)    // movl  8(%ebp), %eax ——%ebp偏置8得到第一个形参xp,存放在%eax

{                                  // movl  12(%ebp), %edx ——%ebp偏置12得到第二个形参y,存放在%edx

    int x = *xp;             // movl  (%eax), %ecx ——%eax存储的地址存储的值赋值给x,x局部变量由%ecx临时存储

    *xp = y;                  // movl  %edx, (%eax) ——将y的值赋值给%eax间接引用的*xp

    return x;                // movl  %ecx, %eax ——返回值也由存储第一个形参的%eax存储

}

函数参数的入口地址就是8(%ebp),而不是4(%ebp)。由于xp是int型占位4字节,因此第二个参数y的地址就是12(%ebp)

至于为什么如此分配寄存器,以后有机会讨论过程链接时再细讲。

从这个例子可以看出,C语言里的所谓指针,在汇编中本质就是地址,谭浩强在写骨灰级教材时,也黑纸白字的写下了指针就是的地址的结论。多年来很多人拿此诟病过他老人家(包括本人在内)。确实,在汇编层面,指针全都退化成地址,但退化的前提是编译器知晓所有细节,站在上帝的视角来退化,当然不会出任何问题。然而作为凡人的我们,如果把指针等价于地址,数组等价于指针,在理解C语言时会出现很多困惑。因此,在C语言中严格把指针当成变量来处理,更符合逻辑。

 

三:特殊mov

假设%dh=AB, %eax=12345678

movb      %dh,%al        //  %eax=123456AB

movsbl   %dh,%eax    //   %eax=FFFFFFAB

movzbl    %dh,%eax    //   %eax=000000AB

 

movb好理解,按照字节来赋值,al是eax的低位,所以从78变成AB

赋值给%eax,剩下三个自己如何处理呢?

movsbl类似于算术扩展,要保留符号位,因此剩下位全置1

movzbl类似于逻辑扩展,因此剩下的全置0。

 

我查了半天也没查到sbl和zbl的命名出处(有同事直接翻译成煞笔和装笔+_+),我的理解是,b和l的解释就像本文开头表上说的那样,b和l分别代表单字节和4字节,两个命令都是执行从单字节传送数据到四字节。而s和z是区分剩下的三个字节的处理策略。这里可以举个很简单的例子:

 

char cC;

unsigned char uC;

 

int  *cP;

unsigned int  *uP;

……cC和uC赋值……;

 

*cp=(char  )uC; //movzbl  %al  %edx  ……单字节无符数转四字节有符数

*uP=(unsigned char )cC; //movsbl  %al  %edx   ……单字节有符数转四字节无符数

 

上面这个例子很典型,当单字节无符数强转四字节有符数时,无符数原本没有带符号位信息,因此在转成有符数时,其余三个字节位就用’0‘来填充,用movzbl。

                                    当单字节有符数强转四字节无符数时,有符数原本是带有符号位信息的,因此在转成无符数时,其余三个字节也要保留符号,用movsbl。

movzbl的操作是百分之百填充’0‘的,而movsbl则不一定。如果被操作的有符数本身就是正数,则符号扩展出来也还是是’0‘,只有被操作有符数本身是负数时,才会出现“1”填充。

 

至于s代表算术操作,z为什么代表逻辑操作,本人没过6级,词汇量有限,就不深究了

 

 

最后

以上就是寒冷枫叶为你收集整理的带你学习《深入理解计算机系统》程序语言的底层描述(1)——汇编基础概念的开始之入门的全部内容,希望文章能够帮你解决带你学习《深入理解计算机系统》程序语言的底层描述(1)——汇编基础概念的开始之入门所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部