<> 二
2020-09-24 本文已影响0人
洧中苇_4187
10.汇编基础
10.1 编写一个C程序,保存为 Hello.c
#include <stdio.h>
int main ()
{
puts("Helloo ,World!\n");
return 0;
}
10.2 使用Clang编译器将其编译成汇编代码
clang -o2 -S -masm=intel -fno-asynchronous-unwind-tables Hello.c
10.3 将汇编代码生成 .o
目标文件
as asm_Hello -o Hello.o
10.4 通过ld指令进行链接,得到可执行文件
ld Hello.o -e _main -lsystem -arch x86_64 -macosx_version_min 10.15.4 -o Hello
说明:
-e(entry) 指定函数入口为 _main ,如果不指定 默认为_start,
源代码中调用了系统函数puts(),所以需要-lsystem 参数链接系统的库,
-arch表示链接为64位的应用程序,
-macosx_version_min则指定了运行需要的最低的系统版本.
双击可执行文件,终端打印如下
Last login: Thu Sep 24 11:45:58 on ttys004
/Users/yangpei1/Desktop/Hello ; exit;
yangpei1@localhost ~ % /Users/yangpei1/Desktop/Hello ; exit;
Helloo ,World!
[进程已完成]
直接创建Xcode工程,运行之前的Hello.s汇编代码,控制台有同样输出
//汇编代码如下
//以(.)开头的指令都是伪指令 剩下的如push mov call等则是X86_64指令,
//.section指示了接下来的代码所位于的段和节取, __TEXT说明位于代码区,
// __text说明是主程序的代码节区,后面的参数则是这个节区的属性
.section __TEXT,__text,regular,pure_instructions
// macOS的最低版本为10.15
.build_version macos, 10, 15 sdk_version 10, 15, 4
//表示使用Intel语法
.intel_syntax noprefix
//_main符号可以被链接器ld使用,因为_main是程序的入口函数,
//所以他必须被链接器使用,并将入口地址写入可执行文件的LC_MAIN加载命令
.globl _main ## -- Begin function main
//代码对齐方式,是2^4=16字节对齐,指令空隙部分使用0x90(nop)进行填充
.p2align 4, 0x90
_main: ## @main
## %bb.0:
push rbp
mov rbp, rsp
sub rsp, 16
mov dword ptr [rbp - 4], 0
lea rdi, [rip + L_.str]
call _puts
xor ecx, ecx
mov dword ptr [rbp - 8], eax ## 4-byte Spill
mov eax, ecx
add rsp, 16
pop rbp
ret
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Helloo ,World!\n"
//伪指令表示当前的截取可以被内联到其他代码中,
//如果没有其他代码使用就可以被剔除掉.
.subsections_via_symbols
11.伪指令
伪指令存在的意义:告诉汇编器指令和代码存放的节区.
.section伪指令只是了接下来代码所在的段区和节区,并指定节区的属性,指令格式为:
.section segname ,sectname [[[,type] ,attribute] ,sizeof_stub]
segname 和 sectname 分别指定段 和 节区的名字,之后的三个参数为可选项,
type指定节的类型;
attribute指定节的属性;
sizeof_stub参数值在type为symbol_stubs时需要,用于指定symbol_stubs的大小
type参数常用的可选的值有以下几种
(1)regular: regular类型可以包含任意代码,链接器不会对他做任何处理
(2)cstring_literals: cstring_literals存储C类型的字符串字面量,
也就是以空字符\0结尾的字符串,只保留一份实例,所有引用这个字符串的地址重定位到该实例上.
(3)4byte_literals,8byte_literals, 16byte_literals:与cstring_literals类似,存放4,8,16字节的字面量
(4)symbol_stubs:由编译器生成,保存未定义的函数的桩代码,一般为动态库中的函数,大小由sizeof_stub指定.
对齐伪指令: .align
X86_64架构在CPU上,以4,8字节对齐的存取速度最快.
数据类型:
(1)整形:整形数据的伪指令.byte, .short, .long, .quad,四种,代表1,2,4,8字节,作用相同,只是定义不同长度的数据结构.
例如:
.data
foo: .byte 1, 2, 3, 4
可以理解为定义了一个byte数组,其中有四个元素,通过 foo 标号引用这个数组.
(2)浮点型: 有4字节单精度浮点型,和 8字节双精度浮点型,分别用 .single 和 .double 进行定义.
(3)字符串: as汇编器提供了两种定义字符串的伪指令, .asciz 用来定义C字符串;另外一种 .ascii 这种会在末尾加 0
举例:
.asciz "Hello"
.ascii "Hello\0"
.byte 'H', 'e', 'l', 'l',-x6f, 0 //0x6f是小写字母o的ASCII码值.
符号导出伪指令:
默认情况下一个编译单元(.s文件)中的符号只在本编译单元可见,在其他编译单元和链接器中是无法使用的,如果要使符号对外可见,需要使用globl伪指令(注意globl,不是global)进行导出,
使用方法:
.globl symbol_name//symbol_name就是要导出的符号名,一般为某个函数或变量的标号
包含头文件的伪指令
.include 和 C 中的 #include效果相同.
12.X86_64汇编基础(Intel 和 AT&T)
寄存器相关说明通用寄存器:
RSP: 栈指针寄存器--该寄存器用于存放当前任务的栈顶地址
RBP: 基址指针寄存器--存放高级语言函数调用中栈帧的基地址
RIP: 指令指针寄存器--存放CPU将要执行的下一条指令的地址(在32位汇编代码中无法使用)
REFLAGS: 标志寄存器--存储着一组影响CPU行为的标志,指示上一条指令的执行状态.
其他寄存器:
浮点寄存器
段寄存器
MMX寄存器
SSE寄存器
AVX寄存器
控制寄存器
调试寄存器
13.汇编语法
lab1: mov rax,123 #将rax寄存器赋值为123
一条汇编指令包含5个部分
lab1 : 指令的标号
mov : 助记符,也就是执行的操作,
rax 和 123,是mov指令需要的两个操作数,
操作数分为三类:立即数,寄存器 或 内存;
X86_64汇编不支持两个操作数都是内存操作数,
目标操作数(这里的rax,这个操作数就是寄存器)也不可以是立即数,
内存寻址:
如何让让指令知道要操作的内存的地址以及大小;内存寻址方式有6种;
size ptr [address]
size表示该操作数的大小,
可以是byte(1字节), word(2字节), dword(4字节), qword(8字节)
address则有6种表达方式
(1)立即寻址: 0x123456 直接跟一个内存地址
(2)寄存器间接寻址: address部分为一个寄存器,寄存器中保存着操作数的内存地址
(3)寄存器相对寻址: 寄存器间接寻址的基础上加一个立即数,
例如: byte ptr [rax + 0x100]
(4)基址加变址寻址: byte ptr [rax + rbx],或者 byte ptr [rax + rbx * 8]
(5)相对基址加变址寻址:在(4)的基础上增加一个偏移量,
例如:byte ptr [rax + rbx * 8 - 0x1234]
(6)在X86_64中新增一种基于RIP寄存器相对寻址,这种寻址方式就是address部分为RIP加上一个偏移量,
例如byte ptr [rip + 0x1234]
操作数大小确定的情况下可以不写 size ptr,内存操作数的大小可以确定
例如mov rax ,qword ptr [rbx],可以简写成 mov rax ,[rbx],
mov [rbx] ,1 这种写法是错误的,因为编译器无法确定目标操作数的大小
address部分如果有偏移量,可以将偏移量写在外面
例如 mov rax,qword ptr [rax + rbx * 8 + 0x1234],可以写成
mov rax,qword ptr 0x1234[rax + rbx * 8]
lea指令用于计算第二个操作数的有效地址,并将它存储到第一个操作数中,,例如:
lea rax ,qword ptr [rax + rbx * 8 + 0x1234]
这条指令会将 rax + rbx * 8 + 0x1234 的计算结果 存储到RAX寄存器中
14.AT&T 与 Intel汇编差异
1.AT&T使用立即数前面加 $, $123,Intel中:123,
2.AT&T使用寄存器前面加 % , %rax,
3.AT&T助记符之后需要增加一个单独的字符串来表示操作数的大小,
比如mov 1,2,4,8字节的操作数时对应 movb, movw, movl , movq
4.AT&T语法中源操作数和目标操作数与Intel语法位置相反,
比如Intel中将RAX寄存器赋值为123,写法为 mov eax,123,
在AT&T需要写成movq $123, %rax
//这段我没看懂~
5.AT&T语法中内存操作数的格式也与Intel语法不同,
比如:mov byte ptr [rax + rbx * 4 + 0x1234],
0x56对应的是AT&T格式为movb $0x56,0x1234(%rax,%rbx,4),mov byte ptr [rax + rbx],
0x12对应的是AT&T格式为 movb $0x12,(%rax,%rbx)
15.数据传送指令 mov/xchg
mov: 将数据从一个操作数复制到另一个操作数,
xchg: 将两个操作数交换,如果某个操作数是内存地址,xchg会对总线进行加锁,保证原子性.
源操作数 和 目标操作数 不能同时为内存地址
16.控制转移指令
image.png1.跳转指令会将RIP设置为操作数所指定的地址,即CPU会从操作数所指定的地址处继续执行;
2.jmp
不执行任何判断,直接将RIP设置为操作数指定的地址,而其他跳转指令则会根据条件进行判断,符合才会设置RIP
3.call
指令相当于将RIP寄存器压入堆栈,然后跳转到操作数指定的地址,
而ret
指令则是从栈中弹出一个值,并将这个值作为地址跳转过去
17.栈操作指令
一个程序运行时的内存一般可简单划分为,
代码区:存放程序的代码
静态数据区:存放静态变量,全局变量
动态数据区: 堆/栈,malloc
函数申请的内存都位于堆区,使用局部变量和函数参数位于栈上.