概述
该主题默认在类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 |
| ||
%ecx %cx |
| ||
%edx %dx |
| ||
%ebx %bx |
| ||
%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等精简指令集的操作就要优美简洁得多,反正我更愿意阅读和写后者的汇编)
假设下面五个地址代表的存储器中分别存储这五个值:
(虚拟)内存地址 | 值 |
0x100 | 0xFF |
0x104 | 0xAB |
0x108 | 0x13 |
0x10C | 0x11 |
以下是三个寄存器所存储的值:
寄存器 | 值 |
%eax | 0x100 |
%ecx | 0x1 |
%edx | 0x3 |
当汇编语句中引用如下操作数所表示的值、对应的寄存器、以及分析如下:
操作数 | 值 | 注释 | 备注 |
%eax | 0x100 | %eax | 调用寄存器,直接出寄存器内的值 |
0x104 | 0xAB | 绝对地址0x104 | 存储器的绝对地址访问,直接出地址内的值 |
$0x108 | 0x108 | 立即数 | 相当于常量 |
(%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)——汇编基础概念的开始之入门所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复