汇编笔记Linux(x86-64)

常用用法

gcc -Og -S XXX.c 将.c程序编译器生成.s的汇编代码

gcc -Og -c XXX.c 将.c程序经过编译器和汇编器申城二进制的.o文件

.o文件中使用gdb用x/14xb XXX(函数名)可以查看在该所处地址开始14个十六进制格式表示的字节

objdump -d XXX.o #这样就可以查看二进制文件的反汇编代码

反汇编器:

  1. 它只是基于机器代码文件中字节序列来确定汇编代码,实际上是不需要访问汇编代码和源代码的
  2. 反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些细微的差距
gcc -Og -o prog main.c mstore.c #生成prog文件
objdump -d prog					#反汇编查看汇编代码

prog文件包含:

  1. 程序代码
  2. 启动和终止程序代码
  3. 与操作系统交互代码

注意区别这个反汇编文件和mstore.o反汇编程序的区别:

  1. 链接器将这段代码的地址移到了一段不同的额地址范围中
  2. 为函数调用找到匹配的函数的可执行代码的位置
  3. 可能在最后几行添加nop指令,为的是是函数代码变成16字节

凿a那个键的程序里不同的寄存器扮演着不同的角色,其中最特别的就是栈指针&rsp,用来指明运行时栈的结束位置。

数据传输指令:将数据从一个位置复制到另外一个位置的指令

MOV类由四种指令组成:movb(1字节), movw(字), movl(双字), movq(四字)

MOV指令在大多数情况下只会更新指定的那些寄存器字节或内存位置,但movl特殊,攘夷寄存器作为目的时,他会把该寄存器的高四字节设置为0

cltq指令,这条指令没有操作数:他总是以寄存器%eax作为源,%rax为符号扩展结果的目的。大的效果与指令movslq $eax,%rax完全一致,不过编码更加紧凑

压入和弹出栈数据

栈是一种数据结构,可以添加或者删除值,但需要遵循“先进后出”的原则。栈可以实现一个数组,总是从数组的一端插入和删除元素

栈向下增长,并且栈顶元素的地址是所有栈中元素地址中最低的(栈指针就是这个位置,他就是地址中最低的)

四字值压入栈的过程实际上是两个过程:1. 栈指针-8 2. 将值写入“新的栈顶地址”(栈顶地址实际上是会变化的)

subq &8,   %rsp		# 将栈指针-8
movq %rbp, (%rsp)	# 将数据存储在栈当中

%rsp 总是指向栈顶,有一点要注意,如果执行完pushq和popq之后,实际上之前存放数据的栈空间依然还是存放的值,会保存到直到下一次被覆盖。

算数和逻辑操作

四组操作:加载有效地址、一元操作、二元操作、移位

  1. 加载有效地址:leaq它的指令形式是从 “内存” 读数据到 “寄存器”,但实际上并没有引用内存,只是把有效地址写入到目的操作数
leaq 7(%rdx, %rdx, 4), %rdx		# 在这里实际上是将5 * %rdx中的值 + 7放入%rdx,并没有访问到地址,并且目的操作数必须是一个									寄存器
  1. 一元和二元操作:如果第二个操作数是内存,首先需要从内存当中读出值,执行操作,再把结果写回内存(sub,add,inc,imul,dec)

  2. 移位操作:SAL和SHL,左移时,两则的效果是一样的,都是讲右边填上0,。右移指令SAR,SHR,其中SAR是填上符号位,SHR执行时是填上0,操作的目的操作数可以使一个寄存器或则是一个内存位置

  3. 特殊的算数操作:

    imulq指令有两种不同的形式。其中一种,是“双操作数”乘法指令,另外一种是“单操作数”

void remdiv(long x, long y
           long *qp, long *rp){
    long q = x/y;
    long r = x%y;
    *qp = q;
    *rp = r;
}
0000000000000000 <remdiv>:
   0:	f3 0f 1e fa          	endbr64 
   4:	48 89 f8             	mov    %rdi,%rax
   7:	49 89 d0             	mov    %rdx,%r8
   a:	48 99                	cqto   #隐含读出%rax的符号位,并将它复制到%rdx的所有位
   c:	48 f7 fe             	idiv   %rsi
   f:	49 89 00             	mov    %rax,(%r8)
  12:	48 89 11             	mov    %rdx,(%rcx)
  15:	c3                   	retq   

以上都是直线代码的行为,其实还有条件语句、循环语句和分支语句–>jump

常见的一些条件吗:

CF:进位标志。最近的操作数使最高位参审了进位。可用来检查无符号操作的溢出。

ZF:零标志。最近的操作得出的结果为0

SF:符号标志。最近的操作得到的结果为负数。

OF:溢出标志。最近的操作导致一个补码溢出----正溢出或负溢出

CMP指令根据两个操作数只差来设置条件吗,除了只设置条件吗而不更新目的寄存器之外,他和SUB指令行为是一样的

TEST指令的行为和AND指令一样,他们除了只设置条件码而不改变目的寄存器的值

movzbl指令不仅会把%eax的高3字节清0,也会把整个寄存器%rax的高4字节都清零

跳转指令

条件传送来实现条件分支:

通过使用“控制”的条件转移。这种方式简单而通用,但是在现代处理器上,他可能会非常低效。

使用“数据”的条件转移。这种方法计算一个条件操作的两种结果,然后再根据条件是否满足从中选取一个。只有在一些受限制的情况中,这种策略才可行,但是如果可行,就可以用一条简单的条件传送指令来实现它。

long absdiff(long x, long y){
	long result;
	if(x<y)
		result = y - x;
	else
		result = x - y;
	return result;
}

#反汇编
absdiff:
	movq %rsi, %rax
	cmp  %rsi, %rdi
	jge  10
	sub  %rdi, %rax
	retq
10	sub	 %rsi, %rdi
	mov  %rdi, %rax
	retq

处理器通过使用流水线来获得高性能,在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操作的一小部分。这种方法通过重叠连续指令的步骤来获得高性能。

无论测试数据是什么,编译出来使用条件传送的代码所需的时间都是大约八个始终周期。控制流不依懒于数据

汇编器可以从目标寄存器的名字推断出条件传送指令的操作数长度,所以对所有的操作数长度,都可以使用同一个指令名字。

循环
do_while循环:

long fact_do(long n){
	long result = 1;
	do{
		result *= n;
		n = n-1;
	}	while(n>1);
	return result;
}

#反汇编
   0:	f3 0f 1e fa          	endbr64 
   4:	b8 01 00 00 00       	mov    $0x1,%eax//这里注意虽然目的寄存器是%eax,但实际上还是会把%rax的高4字节设置为0
   9:	48 0f af c7          	imul   %rdi,%rax
   d:	48 83 ef 01          	sub    $0x1,%rdi
  11:	48 83 ff 01          	cmp    $0x1,%rdi
  15:	7f f2                	jg     9<fact_do+0x9>
  17:	c3                   	retq  
      
while循环:
      1. 跳转到中间,它执行一个无条件跳转跳到循环结尾处的测试,以此来执行初始的测试
      2. 首先使用条件分支,如果初始条件不成立就立即跳出循环
      
for循环:GCC为for循环产生的代码是while循环的两种翻译之一,这取决于优化的等级。
      
过程
  1. 传递控制:P->Q,在P中需要有Q的首地址 “在调用Q的过程中,会把返回地址压入栈” ,在Q的返回时要把程序计数器设置为P中调用Q后面那条指令的地址
  2. 传递数据:P要能够向Q提供一个或多个参数,Q向P返回一个值
  3. 分配和释放内存:再开始时,Q可能需要为局部变量分配空间,在返回前,需要释放这储存空间

当x64-86过程需要的储存空间超过寄存器能够存放的大小时,就会在栈上分配空间,这个部分称为过程的栈帧

转移控制

从Q返回P时,在x86-64机器中,这个信息是用指令call Q调用过程Q来记录的

call指令有一个目标,即指明被调用过程中起始的指令地址。其调用可以使直接的也可以是间接的

数据传输

x86-64中,可以通过寄存器最多传递6个参整形(即证书和指针)参数。寄存器的使用是有特殊顺序的,寄存器的使用的名字取决于数据的类型的大小。 这里寄存器的使用虽然和操作数大小有关,但其访问还是有相应的顺序的:

%rdi -> %rsi -> %rdx -> %rcx -> %r8 -> %r9

如果这个参数大于六,那么超出的部分就用栈来传递。

栈上的局部空间:

在有些时候,局部数据必须存放在内存中,常见的情况包括:

  1. 寄存器不足够存放所有的本地数据
  2. 对一个局部变量使用地址运算符&,因此必须能够为他产生一个地址
  3. 某些局部变量是数组或结构,因此必须通过数组或结构引用被访问到。
定长数组和边长数组:
A in %rdi, i in %rsi, j in %rdx
# 定长数组
leaq (%rsi,%rsi,2), %rax	Compute 3*i
leaq (%rdi,%rax,4), %rax	Compute 12*i
movl (%rax,%rdx,4), %eax	Compute M[Xa + 12*i +4*j]

n in %rdi, A in %rsi, i in %rdx, j in %rcx
# 边长数组
mulq %rdx, %rdi			Compute n*i
leaq (%rsi,%rdi,4),%rax	Compute Xa + 4*n*i
movl (%rax,%rcx,4),%eax Compute M[Xa + 4*n*i + 4*j]

我们可以看出上面的区别:
1. 存储的寄存器不一样
2. 边长数组使用了mulq来计算,一次在一些处理器中,这个惩罚会导致性能下降

异质的数据结构

  1. 结构struct
  2. union,它的每个单位字节是结构中最长的那个字节决定,可以使一种数据类型也可以是一个结构,这里的union要注意一下,因为前文所说的原因,因此,在一个union的结构中,所有的数据都是公用一段共同的地址,这给常重要,这里不仅解释了上面的,也是提示了这是为什么对于union这个字节顺序那么重要

数据对齐

这被称为对齐限制,它简化了形成处理器和内存系统之间接口的硬件设计。

假设一个处理器总是从内存中取8个字节,那么地址就必须是8的倍数,而如果我们能够保证所有的double类型数据的地址对齐成8的倍数,那么就可以用一个内存操作来读或者写,否则就可能需要多个

#比如考虑一下字段
struct S1{
	int i;
	char c;
	int j;
};
想当然的以为是内存给它分配9个字节,其实实际上是分配的12个字节,这个就是遵守了对齐规则。
同时有些时候编译器接狗狗的末尾也有可能需要一些填充,比如
struct S2{
	int i;
	int j;
	char c;
};
这里也是分配12个字节,自己可以好好的试着去思考一下

强制对齐:任何针对x86-64处理器的汇编器和运行时系统都必须保证分配用来保存可能会被SSE寄存器独活蟹的数据结构的内存,都必须满足16字节,这个要求造成两个后果:

  1. 任何内存分配函数生成的块的起始地址都必须是16的倍数
  2. 大所述函数的栈帧的便捷都必须是16字节的倍数
控制和数据结合起来

理解指针:

  1. 每个指针都对应一个类型,这个类型表明该指针指向的是哪一类对象。指针类型不是机器代码中的一部分,它们是C语言提供的一种抽象,避免程序员寻址错误
  2. 每个指针都有一个值。这个值是某个指定类型的对象的地址。
  3. 数组和指针紧密联系
  4. 将指针从一种类型前置转换为另一种类型,只改变他的类型,而不改变它的值

内存越界引用和缓冲区溢出

对越界数组元素的写操作会破坏储存在栈中的信息。一种常见的状态破坏称为缓冲区溢出。

对抗缓冲区溢出攻击
  1. 栈随机化:实现方式是程序开始时,在栈上分配一端0~n字节之间的随机大小的空间。

  2. 栈破坏检测:计算机的第二道防线是能够检测何时栈已经被破坏,在C中我们没有可靠的方式来防止对数组的越界,但是,我们能够这种越界写在造成任何有害结果的时候检测到它。它的思想是,在栈帧中任何局部缓冲区与栈状态之间储存一个特殊的金丝雀值,这个值也称为哨兵值,在每次程序运行时随机产生,并在恢复寄存器状态和从函数返回之前,检测这个函数值是否发生了改变,如果改变,那么就终止程序。

  3. 限制可执行代码区域:消除攻击者向系统中插入可执行代码的能力

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

call me Patrick

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值