计算机执行的是机器代码。机器代码翻译成可读形式就是汇编,也是逆向工程。 在我们使用gcc的时候实际上是生成了汇编的,只是过程被省略了,在学习的时候我们指定优化等级为 -Og
一些在c语言中被省略的细节在汇编语言中是可见的。
程序计数器
: 在x86-64中用%rip
表示,给出将要执行的下一条指令在内存中的位置整数寄存器
包含16个命名的位置,分别存储64位的值。这些寄存器可以存放地址的值或者整数。有的寄存器用来保存一个临时的状态条件码寄存器
保存着最近执行的算术或者逻辑指令的状态信息。用来实现控制流的变化一组向量寄存器
可以存放一个或多个整型或浮点数值 让我们来看书上的关于汇编的一个例子 编写一个mstore.c文件。含有以下内容
long mult2(long, long);
void multstore(long x, long y, long *des) {
long t = mult2(x, y);
*des = t;
}
$gcc -Og -S mstore.c #生成汇编文件
$gcc -Og -c mstore.c #会生成二进制文件(.o)
生成一个.o文件和.s文件 查看.s文件中multstore的定义
里面每一行表示一条机器指令(除了一些伪指令) .o文件里面含有的都是二进制数据 要查看机器代码的内容,我们需要使用反汇编器。在linux里面可以使用objdump命令
$ objdump -d mstore.o
mstore.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <multstore>:
0: f3 0f 1e fa endbr64
4: 53 push %rbx
5: 48 89 d3 mov %rdx,%rbx
8: e8 00 00 00 00 callq d <multstore+0xd>
d: 48 89 03 mov %rax,(%rbx)
10: 5b pop %rbx
11: c3 retq
生成可执行文件还有一步很重要的就是链接。 假设我们有一个main.c文件,含有以下代码
#include <stdio.h>
void multstore(long, long, long *);
int main() {
long d;
multstore(2, 3, &d);
printf("2 * 3 ---> %ld\n", d);
return 0;
}
long mult2(long a, long b) {
long s = a * b;
return s;
}
我们可以用以下方式生成可执行文件prog
gcc -Og -o prog main.c mstore.c
然后反汇编器生成反汇编代码
$ objdump -d prog > prog.s
## 里面有这样一段,可以看到几乎和mstore.c反汇编出来是一样的
00000000000011d5 <multstore>:
11d5: f3 0f 1e fa endbr64
11d9: 53 push %rbx
11da: 48 89 d3 mov %rdx,%rbx
11dd: e8 e7 ff ff ff callq 11c9 <mult2>
11e2: 48 89 03 mov %rax,(%rbx)
11e5: 5b pop %rbx
11e6: c3 retq
11e7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
11ee: 00 00
所有以.
开头的行都是指导链接器和汇编器的伪指令。我们通常忽略这些字段。
数据格式
Intel用术语字
表示16位数据类型。因此,称32位数为双字,称64位数为四字。
多数gcc生成的汇编代码都有一个字符的后缀表明操作数的大小。例如数据传输有四个变种:movb(传送字节)、movw(传送字)、movl(传送双字)、movq(传送四字)。l表示双字。汇编代码也使用后缀l
表示四字节整数和八字节双精度浮点数,因为使用的指令不同,所以不会产生歧义。
16个通用寄存器
操作数指示符
数据传送指令
注意,第一个是源操作数,第二个是目的操作数
将较小的源值复制到较大的目的时有两种扩展方式,零扩展和符号扩展
零扩展是把多出来的位都用0填充,符号扩展则是用符号位填充 cltq
指令没有操作数,总是以寄存器%eax作为源,%rax作为符号扩展的目的
数据传输示例
写这样一段代码
long exchange(long *xp, long y) {
long x = *xp;
*xp = y;
return x;
}
可以看到下面的汇编代码
0000000000000000 <exchange>:
0: f3 0f 1e fa endbr64
4: 48 8b 07 mov (%rdi),%rax
7: 48 89 37 mov %rsi,(%rdi)
a: c3 retq
压入和弹出栈数据
pushq %rbq 等同于
pop %rax 等价于
算术和逻辑操作
加载有效地址
加载有效地址指令leaq实际上是movq指令的变形。它的指令形式是从内存读数据到寄存器,实际上并没有引用内存。 让我们看书中的例子
long scale(long x, long y, long z)
{
long t = x + 4 * y + 12 * z;
return t;
}
反汇编后是
leaq指令能执行加法和有限的乘法
一元和二元操作
第二组的操作是一元操作。第三组是二元操作,第二个操作数既是源又是目的,当第二个操作数为内存地址时处理器必须从内存读出值。
移位操作
最后一组是移位操作,先给出移位量,然后第二项给出的是要移位的数。可以进行算术和逻辑右移。移位量可以是一个立即数,或者放在单字节寄存器%cl中。当%cl中的值为0xFF时,指令salb会移位7位,salw会移15位,sall会移31位,salq会移63位。 左移指令有两种: SAL和SHL。两者的效果是一样的,都是在右边填上0。右移指令则不同,SAR执行算术移位(填上符号位),SHL执行逻辑移位(填上0)。移位操作的目的操作数可以是一个寄存器或是一个内存位置。
特殊的算术操作
imulq是补码乘法,mulq是无符号乘法。这两个操作计算128位乘积,两条指令要求一个参数必须在%rax中,而另外一个作为指令的源操作数给出。然后乘积存放在%rdx(高64位)和%rax(低64位)中,虽然imulq这个名字可以用于两个不同的乘法操作,但是汇编器能够通过计算操作数的数目,分辨出想用哪种指令。来看书中的例子
#include <inttypes.h>
typedef unsigned __int128 uint128_t;
void store_uprod(uint128_t *dest, uint64_t x, uint64_t y) {
*dest=x* (uint128_t) y;
}
我们再来看看如何实现除法
void remdiv(long x, long y, long *qp, long *rp) {
long q = x / y;
long r = x % y;
*qp = q;
*rp = r;
}
控制
机器代码提供两种基本的低级机制来实现有条件的行为:测试数据值,然后根据测试的结果来改变控制流或者数据流
条件码
除了整数寄存器以外。cpu还维护这一组单个位的条件码寄存器。它们描述了最近的算术或逻辑操作的属性。最常用的条件码是
如果计算t = a + b。那么下面的c表达式来设置条件码
访问条件码
条件码通常不会直接读取,常用的使用方法有三种: 1)根据条件码的某种组合将一个字设置为0或者1。2)可以条件跳转到程序的某个其他的部分。3)可以有条件的传送数据。对于第一种,我们称为SET指令
我们来看书上的一个例子
跳转指令
jmp是无条件跳转,其他的是根据比较结果跳转
用条件控制来实现条件分支
让我们来看一个例子
long lt_cnt = 0;
long ge_cnt = 0;
long absdiff_se(long x, long y)
{
long result;
if(x < y)
{
lt_cnt++;
result = y - x;
}
else
{
ge_cnt++;
result = x - y;
}
return result;
}
用条件传送来实现条件分支
传统的方法是通过使用控制的条件转移。当条件满足时,程序沿着一条执行路径执行,当条件不足时,就走另外一条路径。但是可能会非常低效。我们可以使用条件传送指令来实现,更加符合现代处理器的性能特性
今天的文章csapp阅读笔记(3)、程序的机器级表示和x86汇编语言分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16511.html