X86处理器汇编技术系列
第1部分-Linux X86 64位汇编 hello world
汇编让人着迷,本来想温习下,结果变成了一个系列。
准备工作
准备通过一台可以上网的ubuntu系统机器(现在默认是64位机器了),其实其他系统也可以,只是ubuntu方便安装工具,例如nams,所以便于学习。
通过命令#apt install nasm就可以完成汇编器的安装。
引子
每天可以写出大量的程序,例如:
-
#include <stdio.h>
-
-
int main() {
-
int x = 10;
-
printf("hello world,I get %d!\n", x);
-
return 0;
-
}
学过C语言的同学都可以轻松理解这段代码, 但代码在低层是如何工作?并非所有人都能回答这个问题。 我们可以用C,C++,JAVA,Erlang,Go等高级编程语言编写代码,但是编译后不知道它在低级如何工作。
首个64位汇编程序
-
section .data
-
msg db "hello, world!"
-
-
section .text
-
global _start
-
_start:
-
mov rax, 1
-
mov rdi, 1
-
mov rsi, msg
-
mov rdx, 13
-
syscall
-
mov rax, 60
-
mov rdi, 0
-
syscall
先定义了数据段,和字符串常量。
然后代码段和入口。
这里有mov指令,将第二个值给第一个,这里的rax,rdi,rsi,rdx,都是处理器的寄存器。
寄存器可以理解成CPU的御用的存储,CPU指令的数据和指令都来自寄存器。当然寄存器的值来自内存,内存又来自磁盘。
代码中,我们来下个寄存在在代码中的作用。
rax是临时寄存器,当调用系统调用,rax保存系统调用号。第1号系统调用即sys_write.
rdx传递第三个参数给函数,表示字符串大小,这里是13个字符。
rdi传递第一个参数给函数,是函数句柄,表示stdout。
rsi传递第二个参数给函数,是字符串所在地址。
我们在内核源码文件fs/read_write.c文件中可以到找如下定义:
size_t sys_write(unsigned int fd, const char * buf, size_t count);
最后传递60给rax寄存,然后传递0给rdi寄存器,第60号的系统调用是exit。
编译
编译命令很简单:
nasm -f elf64 -o hello.o hello.asmld -o hello hello.o
通过nasm进行编译,然后通过ld进行连接,执行./hello后可以看到有字符串输出。
这个就是我们的第一个汇编程序代码。
(apt install nasm即可在ubuntu上完成nasm安装)
NAMS介绍:
NASM全称The Netwide Assembler,是一款基于80x86和x86-64平台的汇编语言编译程序,其设计初衷是为了实现编译器程序跨平台和模块化的特性。NASM支持大量的文件格式,包括Linux,*BSD, ELF,COFF等,同时也支持简单的二进制文件生成。语法相较Intel的语法更为简单,支持目前已知的所有x86架构之上的扩展语法,同时也拥有对宏命令的良好支持。
Linux 平台的标准汇编器是 GAS,它是 GCC 所依赖的后台汇编工具,通常包含在 binutils 软件包中。GAS 使用标准的 AT&T 汇编语法,可以用来汇编用 AT&T 格式编写的程序。我们在后面也会一并学习到。
我们这里前面还是使用Intel汇编来回顾下,后面会切到AT&T汇编上。
关于nasm语法
汇编所有语法不能一下子全部描述完毕,这里我们会根据文章逐步提到需要。
在刚才的代码,我们会遇到两个段。
数据段和代码段。
数据库用于声明常数,不会再运行中变化。
定义数据段如下:
section .data
代码段用
section .text, 必须使用global _start开始,表示程序执行的开始位置。
第2部分-Linux x86 64位汇编Intel汇编语法一
上篇中,我们提到的语法都是基于Intel的汇编语法。
与之对应的是AT&T汇编,也是Linux内核中的汇编语法。
我们先学习intel汇编,主要是Intel的汇编和大学里面的教程一致,更加顺手。
先来复习下几个概念,然后会增加例子来进行实践
汇编中的术语和概念
第一步部分中只是个引子,这部分中进行术语的描述。
寄存器,是位于处理器中的小存储。处理器可以从内存获取数据,但是很慢,所以需要内部存储数据的存储即寄存器。有16个通用的寄存器:
rax, rbx, rcx, rdx, rbp, rsp, rsi, rdi, r8, r9, r10, r11, r12, r13, r14, r15.
栈:因为处理只有非常有限数量的寄存器,所以栈是处理器通过特殊寄存器进行寻址的连续区域。
段: data数据段用于声明初始化数据或常量
bss用于声明非初始化变量
text用于代码。
数据类型
先来看下整型数,基础数据类型是:byte,word,doubleword,quadword和double quadword.
一个byte是8位,一个word是2个byte,一个doubleword是4个bytes, 一个quadword是8个字节,一个double quadword是16个字节。
整型分为有符号和无符号的。例如,8位的无符号范围是0-255,有符号的事-128到127。
例如如下:
-
section .data
-
num1: equ 10
-
num2: equ 5
-
msg: db "Num is correct", 10
这里定义里3个常量num1,num2,msg。这里的db是NASM的伪指令。
常用的伪指令有: DB, DW, DD, DQ, DT, DO, DY,DZ,用于声明初始化数据。
RESB, RESW, RESD, RESQ, REST, RESO, RESY and RESZ用于声明费初始化变量。
INCBIN包含外部二进制文件
EQU定义常量。
TIMES反复执行数据指令。
算法操作
ADD 整数加
SUB 减法
MUL 无符号乘
IMUL 符号乘
DIV 无符号除
IDIV 符号除
INC 增加
DEC 减法
NEG 取反
控制流
例如if,while,for
汇编中有cmp指令。
对比两个值,后面会接调整指令如下:
JE 相等跳转
JZ 等于0调整
JNE 不相等跳转
JNZ 不等于0调整
JG 第一个值大于第二个值
JGE 第一个值大于等于第二个值
JA 类似JG,但是对比无符号对比。
JAE 类似JGE,但是对比无符号对比。
实例
-
section .data
-
num1: equ 100
-
num2: equ 50
-
msg: db "Sum is correct\n"
-
-
section .text
-
-
global _start
-
-
;; entry point
-
_start:
-
mov rax, num1
-
mov rbx, num2
-
add rax, rbx
-
cmp rax, 150
-
jne .exit
-
jmp .right
-
-
; Print message that sum is correct
-
.right:
-
mov rax, 1
-
mov rdi, 1
-
mov rsi, msg
-
mov rdx, 15
-
syscall
-
jmp .exit
-
-
.exit:
-
mov rax, 60
-
mov rdi, 0
-
syscall
编译连接运行
nasm -f elf64 -o addsum.o addsum.asmld -o addsum addsum.o
然后可以执行./addsum
第3部分-Linux x86 64位汇编Intel汇编语法二
特殊符号
$和$$是编译器 NASM 预留的关键字,用来表示当前行和本 section 的地址,起到了标号的作用,是 NASM 提供的,并不是 CPU 原生支持的,相当于伪指令一样。
$是编译器给当前行安排的地址,每行都有。
$$指代本 section 的起始地址,此地址同样是编译器给安排的。
nasm 默认全部代码同为一个 section,起始地址为 0。section 也称为节、段,程序中的一小块。
vstart=来修饰后,可以被赋予一个虚拟起始地址 virtual start address
汇编乘法MUL
MUL是进行无符号乘法的指令。MUL(无符号乘法)指令有三种格式:第一种是将8位的操作数于al相乘(乘积位于ax)。第二种是将16位的操作数与ax相乘(乘积位于dx:ax); 第三种是将32位的操作数与eax进行相乘(乘积位于edx:eax)。
7种寻址方式
基址寻址、变址寻址、基址变址寻址,这三种形式中的基址寄存器只能是 bx、 bp,变址寄存器只能是 si、 di。其中 bx 默认的段寄存器是 ds,经常用于访问数据段, bp默认的段寄存器是 ss,它经常用于访问栈。
前缀指令
定义(define)变量时就用5个不同的关键字:DB,DW,DD,DQ,DT
DW(DEFINE WORD)定义一个字(两个字节)长度
DD(DEFINE DOUBLE WORD)定义双字(4个字节)长度
DQ(DEFINE QUARTET WORD)定义四字(8个字节)长度
DT(DEFINE TEN BYTE)定义十字节长度
堆栈
第4部分-Linux x86 64位汇编Intel汇编语法三
栈是FIFO结构。
64位的X86处理器有16个通用寄存器RAX, RBX, RCX, RDX, RDI, RSI, RBP, RSP and R8-R15。对于应用来说太少了,所以需要存储数据到栈中。
栈的另一个用途是,调用函数时候,地址通过压栈,当函数执行结束后可以返回地址,在原先地方继续执行。
函数调用
例如:
-
global _start
-
-
section .text
-
-
_start:
-
mov rax, 1
-
call incRax
-
cmp rax, 2
-
jne exit
-
;…………
-
-
-
incRax:
-
inc rax
-
ret
调用函数incRax,将rax值加1.这个是个代码片段不能直接执行。
根据System V AMD64 ABI规范我们,函数的前6个参数是通过寄存器来传递,其他的通过栈来传递。
rdi是第一个参数
rsi是第二个参数
rdx是第三个参数
rcx是第四个参数(如果系统调用函数的话是r10寄存器)
r8是第五个参数
r9是第六个参数
X86-64系统调用使用syscall指令.该指令将返回地址保存到rcx,会破坏rcx。所以使用r10寄存器了。
栈指针
RBP是基地址寄存器。指向当前栈的基地址。RSP是栈指针,指向当前栈的顶部。
栈的操作是两个:
压栈PUSH,增加RSP,并保存参数到栈指针指向的位置。
出栈POP,复制数据从栈指针指向的位置到参数。
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方式是向下的,是向着内存地址减小的方向增长。
栈操作实例
看下例子,先定义了常量如下:
-
section .data
-
SYS_WRITE equ 1
-
STD_IN equ 1
-
SYS_EXIT equ 60
-
EXIT_CODE equ 0
-
-
NEW_LINE db 0xa
-
WRONG_ARGC db "Must be two command line argument", 0xa
-
-
section .text
-
global _start
-
-
_start:
-
pop rcx ;//栈的第一个保存的是参数的数量,数量不为3则跳转到argcError处退出。
-
cmp rcx, 3 ;//两个参数,外加一个程序名,程序名也是参数。
-
jne argcError
-
-
add rsp, 8;//[rsp+8]保存的是第一个参数argv[0],跳过程序名这个变量。
-
pop rsi;//将第一个参数赋值给rsi。
-
call str_to_int;调用函数str_to_int,将参数转换为整型,保存于rax。
-
mov r10, rax;保存到r10寄存器中
-
pop rsi;将第二个参数赋值为rsi
-
call str_to_int;调用函数str_to_int,字符串保存的,例如“123\0”
-
mov r11, rax;保存到r11寄存器中
-
add r10, r11;完成加法。
-
mov rax, r10
-
xor r12, r12
-
jmp int_to_str;调用函数int_to_str函数,整型转换为字符串
-
-
argcError:
-
mov rax, 1
-
mov rdi, 1
-
mov rsi, WRONG_ARGC
-
mov rdx, 34
-
syscall
-
jmp exit
-
-
str_to_int:;负责将字符串转换为整型
-
xor rax, rax;清空rax寄存器
-
mov rcx, 10;赋值rcx为10
-
next:
-
cmp [rsi], byte 0;对比参数的低位字节,是否为0,字符串最后一个为’\0’
-
je return_str;为0,则调用函数return_str返回
-
mov bl, [rsi];否则将低8位赋值给bl
-
sub bl, 48;参数减去48,ASCII码中,字符和数字相差48。
-
mul rcx;乘法,乘以10
-
add rax, rbx;
-
inc rsi;增加rsi,即变为下一个字节。
-
jmp next;调到函数next
-
-
return_str:
-
ret;直接返回。
-
-
int_to_str:
-
mov rdx, 0
-
mov rbx, 10
-
div rbx;除以10,获取个位数余数在rdx,商在rax。
-
add rdx, 48
-
add rdx, 0x0
-
push rdx
-
inc r12;首次迭代为0,记录字符的个数,用于后续输出。
-
cmp rax, 0x0;商是否为0,为0则退出,跳转到print进行输出。
-
jne int_to_str;商不为0,则继续输出。
-
jmp print
-
-
print:
-
mov rax, 1
-
mul r12
-
mov r12, 8
-
mul r12
-
mov rdx, rax
-
-
mov rax, SYS_WRITE
-
mov rdi, STD_IN
-
mov rsi, rsp
-
;; call sys_write
-
syscall;调用sys_write输出结果
-
jmp exit
-
-
exit:
-
mov rax, SYS_EXIT
-
mov rdi, EXIT_CODE
-
syscall
编译连接运行
nasm -g -f elf64 -o addsum_argu.o addsum_argu.asmld -o addsum_argu addsum_argu.o
然后可以执行
./addsum_argu 12 3045
可以实现加法运算。
这里可以有些细节不能一下子全部注释清楚。
第5部分-Linux x86 64位汇编 AT&T汇编
关于调试我们放到后面,因为这篇开始还是进入本系列的正题了。
学习了前面的INTEL 汇编,开始使用AT&T汇编了。
不是所有汇编器使用的标准都一样的,不通汇编器使用不同的汇编语法。
关于AT&T汇编,也就是基于gas汇编器的。可以参考书籍《Programming Ground Up》。
AT&T汇编程序的结构跟其它汇编语言类似,由directives, labels, instructions组成,助记符最多可以跟随三个操作数。与Intel汇编语言相比,最大的区别在于操作数的顺序。
这里要提醒的是:这些都会在实例中来学习巩固,死记还是比较难的,在实践中不断理解即可。
比如Intel汇编语法的传送指令通常是:
mnemonic destination,source
在AT&T汇编中,通常是:
mnemonic source,destination
- 也即源操作数在左边,目的操作数在右边。
- 所有寄存器前面都要加前缀%.
- 所有立即数必须冠以前缀$.
- 符号常数直接引用,引用符号地址在符号前加符号$
- 通过对操作指令添加b/w/l/q等后缀来指示操作数“尺寸”. Intel 语法通过在内存操作数(而不是操作码本身)前面加 byte ptr、word ptr 和 dword ptr 来指定大小。
jmp, call, ret这些指令用于控制程序从某条指令跳到另外的指令处。它又分为近跳转(在同一段内)和远跳转(不同段内)。跳转地址可以用相对偏移(label),寄存器,内存操作数.
- 中间形式长跳转和调用是 lcall/ljmp $section, $offset;Intel 语法是 call/jmp far section:offset。在 AT&T 语法中,远返回指令是 lret $stack-adjust,而 Intel 使用 ret far stack-adjust。
- GAS中的内存操作数的寻址方式是section:disp(base, index, scale)。%segment:ADDRESS (, index, multiplier)或%segment:(offset, index, multiplier)或%segment:ADDRESS(base, index, multiplier)。ADDRESS or offset + base + index * multiplier。NASM 使用的语法比较简单。上面的公式在 NASM 中表示为:Segment:[ADDRESS or offset + index * multiplier]
- GAS 中,重复结构以.rept指令开头,用一个.endr指令结束这个指令。.rept后面是一个数字,指定.rept/.endr结构中表达式重复执行的次数。相当于编写这个指令count次,每次重复占据单独的一行。NASM 中,在预处理器级使用相似的结构。它以 %rep 指令开头,以%endrep结尾。%rep指令后面是一个表达式。NASM 中还有另一种结构,times指令。与%rep相似,它也在汇编级起作用。
在AT&T语法中,符号扩展和零扩展指令的格式为,基本部分"movs"和"movz"(对应Intel语法的movsx和movzx),后面跟源操作数长度和目的操作数长度。movsbl意味着movs (from)byte (to)long;movsbw意味着movs (from)byte (to)word;movswl意味着movs (from)word (to)long。对于movz指令也一样。
以点号开头的任何内容都不会直接转换为机器指令。例如.section .data表示数据段,.section .text表示代码段,.globl意味着汇编程序在汇编后不应丢弃此符号,因为链接器将需要它。 _start是一个特殊的符号,总是需要用.globl标记,因为它标记了程序开始的位置。
函数定义.type funcname,@function,定义函数。
- GAS 中的汇编器指令以 “.” 开头,但是在 NASM 中不是。
- GAS 支持 C 风格(/* */)、C++ 风格(//)和 shell 风格(#)的注释。NASM 支持以 “;” 字符开头的单行注释。
- NASM 分别使用 dd、dw 和 db 指令声明 32 位、16 位和 8 位数字,而 GAS 分别使用 .long、.int 和 .byte。GAS 还有其他指令,如 .ascii、.asciz 和 .string。在 GAS 中,像声明其他标签一样声明变量(使用冒号),在 NASM 中,只需在内存分配指令(dd、dw 等等)前面输入变量名,后面加上变量的值。
- 内存直接寻址模式。NASM 使用方括号间接引用一个内存位置指向的地址值:[var1]。GAS 使用圆括号间接引用同样的值:(var1)
- 字符串的地址 ,在 NASM 中,内存变量代表内存位置本身,所以 push str 这样的调用实际上是将地址压入堆栈的顶部。在 GAS中,变量 str 必须加上前缀 $,才会被当作地址。如果不加前缀 $,那么会将内存变量代表的实际字节,而不是地址。
- 关于注释,Nasm是;,而在AT&T中是# 单行注释;// 单行注释;/* */ 多行注释
当然这些都会在实例中来学习巩固,死记还是比较难的,实践中不断理解即可。
数据声明
AT&T中数据声明如下:
命令 数据类型
.ascii 文本字符串
.asciz 以空字符串结尾的文本字符串
.byte 字节值
.double 双精度浮点数
.float 单精度浮点数
.int 32位整数
.long 32位整数(同32)
.octa 16字节整数
.quad 8字节整数
.short 16位整数
.single 单精度浮点数(和.float同)
此外汇编器使用两个命令声明缓冲:
命令 描述
.comm 声明未初始化的数据的通用内存区域
.lcomm 声明未初始化的数据的本地通用内存区域
movx,其中x可以是下面字符:
x 描述
q 用于64位的长字值
l 用于32位的长字值
w 用于16位的字值
b 用于8位的字节值
后面我们会给出几个常用的AT&T语法汇编例子。
第6部分-Linux x86 64位汇编 AT&T汇编示例一
示例——退出
最简单的汇编推出示例如下,
-
-
.section .data
-
-
.section .text
-
.globl _start
-
_start:
-
-
movl $1, %eax # 退出程序的调用码
-
movl $0, %ebx #返回给操作系统的状态
-
-
# 调用内核执行退出代码
-
int $0x80
-
进行汇编,得到对象文件。
#as exit.s -o exit.o
然后通过连接器将对象文件放在一起并加入信息,这样内核知道如何加载和运行。
# ld exit.o -o exit
然后执行./exit,执行完毕后执行
#echo $?
输出结果。
第7部分-Linux x86 64位汇编 AT&T汇编示例二
示例——找出最大值
代码max.s如下:
-
# %edi - 被检查数据条目的索引
-
# %ebx – 最大数据条目
-
# %eax – 当前数据条目
-
# # The following memory locations are used:
-
# # data_items – 保持数据条目. 0用于接收数据条目
-
-
.section .data
-
data_items: #数据
-
.long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
-
-
.section .text
-
.globl _start
-
_start:
-
-
movl $0, %edi #移动0到索引寄存器
-
movl data_items(,%edi,4), %eax #加载第一个数据
-
movl %eax, %ebx # 第一个数据为最大
-
-
start_loop: # 启动循环
-
-
cmpl $0, %eax # 确认是否到最后一个数据
-
je loop_exit
-
incl %edi # 加载到下一个值
-
movl data_items(,%edi,4), %eax
-
cmpl %ebx, %eax # 对比值
-
jle start_loop # 是否大于原最大值
-
movl %eax, %ebx # 更新最大值
-
-
jmp start_loop
-
-
loop_exit:
-
-
movl $1, %eax # exit() 系统调用
-
int $0x80
-
编译:as max.s -o max.o
链接:ld -o max max.o
执行./max, 然后通过#echo $? 进行输出结果。
第8部分-Linux x86 64位汇编 AT&T汇编示例三
示例——求指数
这里几个例子还是使用的32位的寄存器,后面会渐渐过渡到64位的寄存器。
在求指数中,我们定义了函数,通过函数来实现指数求解(这里实现的23+52=33,将参数写入了代码中)。
-
.section .data
-
-
.section .text
-
-
.code32
-
.globl _start
-
_start:
-
-
pushl $3 #压栈第二个参数
-
pushl $2 #压栈第一个参数
-
call power #调用函数
-
addl $8, %esp #移动栈指针
-
-
pushl %eax #保存第一个结果
-
-
pushl $2 #压栈第二参数
-
pushl $5 ##压栈第一个参数
-
call power #调用函数
-
addl $8, %esp #移动栈指针
-
-
popl %ebx #第二个值在%eax中,第一个值在栈中
-
-
addl %eax, %ebx #将他们相加在%ebx
-
-
movl $1, %eax #退出
-
int $0x80
-
-
#INPUT: 第一个参数-基数 %ebx
-
# 第二个参数-指数 %ecx
-
# -4(%ebp) 保存当前结果
-
# %eax 用于临时存储
-
#OUTPUT:
-
# 结果返回
-
-
.type power, @function
-
power:
-
-
pushl %ebp #保存旧的基指针
-
movl %esp, %ebp #将栈指针移动给基指针
-
subl $4, %esp #为本地变量腾出空间
-
-
movl 8(%ebp),%ebx #第一个参数放入到%eax
-
movl 12(%ebp),%ecx #第二个参数放入到%ecx
-
-
movl %ebx, -4(%ebp) #存储当前结果
-
-
power_loop_start:
-
-
cmpl $1, %ecx #如果结果 是1,则结束
-
je end_power
-
movl -4(%ebp), %eax #移动当前结果到%eax
-
imull %ebx, %eax #用于基数乘以当前结果
-
movl %eax, -4(%ebp) #存储结果
-
-
decl %ecx
-
jmp power_loop_start
-
-
end_power:
-
movl -4(%ebp), %eax
-
movl %ebp, %esp
-
popl %ebp
-
ret
-
#as -o power.o power.s --32
# ld -o power power.o -melf_i386
#./power
通过echo $?可以得到33.
第9部分-Linux x86 64位汇编 AT&T汇编示例四
示例——输出字符串
如下代码示例:
-
.data # 数据段声明
-
msg : .string "Hello, world!\\n" # 要输出的字符串
-
len = . - msg # 字串长度
-
.text # 代码段声明
-
.global _start # 指定入口函数
-
-
_start: # 在屏幕上显示一个字符串
-
movl $len, %edx # 参数三:字符串长度
-
movl $msg, %ecx # 参数二:要显示的字符串
-
movl $1, %ebx # 参数一:文件描述符(stdout)
-
movl $4, %eax # 系统调用号(sys_write)
-
int $0x80 # 调用内核功能
-
-
# 退出程序
-
movl $0,%ebx # 参数一:退出代码
-
movl $1,%eax # 系统调用号(sys_exit)
-
int $0x80 # 调用内核功能
-
通过as汇编器进行编译。
# as -o hello.o hello.s
#ld -s -o hello hello.o
其中-s参数表示在链接时候去掉函数符号。
第10部分-Linux x86 64位汇编 函数调用规则
汇编语言程序中创建函数需要3个步骤。
- 定义需要的输入值
- 定义对输入值执行的操作
- 定义如何生成输出值以及如何把输出值传递给发出调用的程序。
定义输入值
可以使用寄存器,全局变量或者堆栈。
使用寄存器时候要注意,如果被调用的函数修改主程序使用的寄存器,那么在被调用之前保存寄存器的当前状态,并且在函数返回之后恢复寄存器的状态。可以使用PUSH和POP,或者PUSHA和POPA。
全局变量是在内存中的位置,程序中所有函数都可以访问这些内存位置。
定义函数处理
汇编器中声明函数名称如下:
.type func1,&function
func1:
函数结束由RET指令定义,执行到RET指令时,程序控制返回主程序,返回的位置是紧跟在调用函数的CALL指令后面的指令。
和高级语言不通,可以把任意数量的函数代码放在_start之前也可以放在主程序之后。
定义输出值
和输入值类似,输出结果可以是下面两种常见的
把结果存放在一个或多个寄存器中
把结果存放在全局变量内存位置中。
访问函数
创建好函数后,就可以在程序中的任何位置进行访问。使用CALL指令用于把控制从主程序传递到函数。
call function
第11部分-Linux x86 64位汇编 函数完整示例
调用函数示例
这里开始使用到64位的寄存器了,编译的也是64位可执行程序了。
-
.extern printf ;//调用外部的printf函数
-
.section .data
-
-
result:
-
.ascii "Area result is %f.\n"
-
.byte 0x0a,0x0
-
-
precision:
-
.byte 0x7f, 0x00
-
.section .bss
-
.lcomm value, 4
-
.lcomm areasize, 8
-
-
.section .text
-
.globl _start
-
_start:
-
nop
-
finit;//初始化FPU
-
fldcw precision;//加载精度到FPU的控制寄存器
-
-
movl $10, %ebx
-
call area;//调用函数area
-
movq $result,%rdi
-
fstpl areasize
-
movq areasize, %xmm0
-
call printf
-
-
-
movl $2, %ebx
-
call area;//调用函数area
-
movq $result,%rdi
-
fstpl areasize
-
movq areasize,%xmm0
-
call printf
-
-
-
movl $120, %ebx
-
call area;//调用函数area
-
movq $result,%rdi
-
fstpl areasize
-
movq areasize,%xmm0
-
call printf
-
-
mov $60,%rax
-
syscall
-
-
.type area, @function;//定义函数area
-
area:
-
fldpi;//加载π
-
imull %ebx, %ebx;//半径相乘
-
movl %ebx, value;//半径相乘结果移动到value
-
filds value;//加载半径相乘结果到st0,π移动到st1
-
fmulp %st(0), %st(1) ;//求面积,保存到栈顶st0
-
ret
as -o functest.o functest.s
ld -o functest functest.o -lc -I /lib64/ld-linux-x86-64.so.2
汇编语言||基本传送指令MOV的用法详解
MOV指令
MOV指令,能实现以下操作:
- CPU内部寄存器之间数据的任意传送(除了码段寄存器CS和指令指针IP以外)。
- 立即数传送至CPU内部的通用寄存器组(即AX、BX、CX、DX、BP、SP、SI、DI),给这些寄存器赋初值。
- CPU内部寄存器(除了CS和IP以外)与存储器(所有寻址方式)之间的数据传送,可以实现一个字节或一个字的传送。
- 能实现用立即数给存储单元赋初值。
其中:
所以,注意MOV的使用范围
下面给出一些具体示例:
立即数传送:
MOV CL,4 ;CL←4,字节传送
MOV DX,0FFH ;DX←00FFH,字传送
MOV SI,200H ;SI←0200H,字传送
MOV BVAR,0AH ;字节传送 ;假设BVAR是一个字节变量,定义如下:BVAR DB 0
MOV WVAR,0BH ;字传送 ;假设wvar是一个字变量,定义如下:wvar dw 0
寄存器传送
mov ah,al ;ah←al,字节传送
mov bvar,ch ;bvar←ch ,字节传送
mov ax,bx ;ax←bx,字传送
mov ds,ax ;ds←ax,字传送
mov [bx],al ;[bx]←al,字节传送
存储器传送:
mov al,[bx] ;al←ds:[bx]
mov dx,[bp] ;dx←ss:[bp+0]
mov dx,[bp+4] ;dx←ss:[bp+4]
mov es,[si] ;es←ds:[si]
段寄存器传送:
MOV [SI],DS
MOV AX,DS ;AX←DS
MOV ES,AX ;ES←AX←DS
第12部分-Linux x86 64位汇编 函数C样式传递
因为输入值和输出值可用的选择很多,这样哪个函数使用哪些寄存器和全局变量,或者哪些寄存器和全局变量传递哪些参数会是程序员的噩梦。
为此需要一个标准,使用某一标准一致地存放输入参数以便函数获取,一致地存放输出值便于主程序获取。
C把输入值传递给函数的解决方案是使用堆栈。C定义了返回主程序的值是用EAX,浮点用ST(0).
C样式要求参数存放到堆栈中的顺序和函数的原型中的顺序相反。
-
.extern printf ;//调用外部的printf函数
-
.section .data
-
precision:
-
.byte 0x7f, 0x00
-
resultstr:
-
.ascii "Area result is %f.\n"
-
-
.section .bss
-
.lcomm bresult, 4
-
-
.section .text
-
.globl _start
-
_start:
-
nop
-
finit;//FPU初始化
-
fldcw precision;//加载FPU控制器
-
push $10;//加载10到栈中
-
call area;//调用area函数
-
addq $8, %rsp;//增加8个字节,即跳过压栈的参数
-
movl %eax, bresult;//保存结果到result
-
-
movq bresult,%xmm0;//结果复制到xmm0寄存器
-
cvtps2pd %xmm0,%xmm0;//转化单精度为双精度
-
movq $resultstr,%rdi
-
call printf
-
-
push $2
-
call area
-
addq $8, %rsp
-
movl %eax, bresult
-
movq $resultstr,%rdi
-
movq bresult,%xmm0
-
cvtps2pd %xmm0,%xmm0
-
call printf
-
-
push $120
-
call area
-
addq $8, %rsp
-
movl %eax, bresult
-
movq $resultstr,%rdi
-
movq bresult,%xmm0
-
cvtps2pd %xmm0,%xmm0
-
call printf
-
-
mov $60,%rax
-
syscall
-
-
.type area, @function;//定义函数area
-
area:
-
push %rbp;//压栈rbp寄存器
-
mov %rsp, %rbp;//将rsp赋值为rbp,,rsp <- rbp
-
subq $8, %rsp;//设置rsp网上走,这里其实没有实际作用,64位系统中一个压栈就是8个字节
-
fldpi;//加载π到st0, FLD是Intel的指令集协处理器的汇编指令,用于把浮点数字传送入和传送出FPU寄存器。
-
filds 16(%rbp) ;//加载rbp+8就是调用函数前压栈的参数到st0,π移到st1,fild是将整数转化为长双精FP80压栈(压到st0) fstp是将弹栈指令,将st0弹出
-
fmul %st(0), %st(0) ;//st0和st0相乘,保存在st0
-
fmulp %st(0), %st(1) ;//st0和st1相乘,结果在栈顶st0
-
fstps -8(%rbp) ;//保存结果到栈中, 8个字节,也就是挡墙rsp所指的位置。
-
movl -8(%rbp), %eax;//最后将结果移动到eax寄存器,是单精度值,汇编指令 movmovb (8位)、movw (16位)、 movl (32位)、movq (64位)
-
-
mov %rbp, %rsp;//恢复esp寄存器(->ebp)
-
pop %rbp;//恢复ebp寄存器
-
ret;//返回函数
as -g -o ccalltest.o ccalltest.s
ld -o ccalltest ccalltest.o -lc -I /lib64/ld-linux-x86-64.so.2