X86处理器汇编技术系列

第1部分-Linux X86 64位汇编 hello world

汇编让人着迷,本来想温习下,结果变成了一个系列。

准备工作

准备通过一台可以上网的ubuntu系统机器(现在默认是64位机器了),其实其他系统也可以,只是ubuntu方便安装工具,例如nams,所以便于学习。

通过命令#apt install nasm就可以完成汇编器的安装。

引子

每天可以写出大量的程序,例如:

 
  1. #include <stdio.h>
  2.  
  3. int main() {
  4. int x = 10;
  5. printf("hello world,I get %d!\n", x);
  6. return 0;
  7. }
 

学过C语言的同学都可以轻松理解这段代码, 但代码在低层是如何工作?并非所有人都能回答这个问题。 我们可以用C,C++,JAVA,Erlang,Go等高级编程语言编写代码,但是编译后不知道它在低级如何工作。

首个64位汇编程序

 
  1. section .data
  2. msg db "hello, world!"
  3.  
  4. section .text
  5. global _start
  6. _start:
  7. mov rax, 1
  8. mov rdi, 1
  9. mov rsi, msg
  10. mov rdx, 13
  11. syscall
  12. mov rax, 60
  13. mov rdi, 0
  14. 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。

例如如下:

 
  1. section .data
  2. num1: equ 10
  3. num2: equ 5
  4. 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,但是对比无符号对比。

实例

 
  1. section .data
  2. num1: equ 100
  3. num2: equ 50
  4. msg: db "Sum is correct\n"
  5.  
  6. section .text
  7.  
  8. global _start
  9.  
  10. ;; entry point
  11. _start:
  12. mov rax, num1
  13. mov rbx, num2
  14. add rax, rbx
  15. cmp rax, 150
  16. jne .exit
  17. jmp .right
  18.  
  19. ; Print message that sum is correct
  20. .right:
  21. mov rax, 1
  22. mov rdi, 1
  23. mov rsi, msg
  24. mov rdx, 15
  25. syscall
  26. jmp .exit
  27.  
  28. .exit:
  29. mov rax, 60
  30. mov rdi, 0
  31. 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。对于应用来说太少了,所以需要存储数据到栈中。

    栈的另一个用途是,调用函数时候,地址通过压栈,当函数执行结束后可以返回地址,在原先地方继续执行。

函数调用

例如:

 
  1. global _start
  2.  
  3. section .text
  4.  
  5. _start:
  6. mov rax, 1
  7. call incRax
  8. cmp rax, 2
  9. jne exit
  10. ;…………
  11.  
  12.  
  13. incRax:
  14. inc rax
  15. 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,复制数据从栈指针指向的位置到参数。

 

    对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方式是向下的,是向着内存地址减小的方向增长。

 

栈操作实例

看下例子,先定义了常量如下:

 
  1. section .data
  2. SYS_WRITE equ 1
  3. STD_IN equ 1
  4. SYS_EXIT equ 60
  5. EXIT_CODE equ 0
  6.  
  7. NEW_LINE db 0xa
  8. WRONG_ARGC db "Must be two command line argument", 0xa
  9.  
  10. section .text
  11. global _start
  12.  
  13. _start:
  14. pop rcx ;//栈的第一个保存的是参数的数量,数量不为3则跳转到argcError处退出。
  15. cmp rcx, 3 ;//两个参数,外加一个程序名,程序名也是参数。
  16. jne argcError
  17.  
  18. add rsp, 8;//[rsp+8]保存的是第一个参数argv[0],跳过程序名这个变量。
  19. pop rsi;//将第一个参数赋值给rsi。
  20. call str_to_int;调用函数str_to_int,将参数转换为整型,保存于rax。
  21. mov r10, rax;保存到r10寄存器中
  22. pop rsi;将第二个参数赋值为rsi
  23. call str_to_int;调用函数str_to_int,字符串保存的,例如“123\0
  24. mov r11, rax;保存到r11寄存器中
  25. add r10, r11;完成加法。
  26. mov rax, r10
  27. xor r12, r12
  28. jmp int_to_str;调用函数int_to_str函数,整型转换为字符串
  29.  
  30. argcError:
  31. mov rax, 1
  32. mov rdi, 1
  33. mov rsi, WRONG_ARGC
  34. mov rdx, 34
  35. syscall
  36. jmp exit
  37.  
  38. str_to_int:;负责将字符串转换为整型
  39. xor rax, rax;清空rax寄存器
  40. mov rcx, 10;赋值rcx为10
  41. next:
  42. cmp [rsi], byte 0;对比参数的低位字节,是否为0,字符串最后一个为’\0
  43. je return_str;为0,则调用函数return_str返回
  44. mov bl, [rsi];否则将低8位赋值给bl
  45. sub bl, 48;参数减去48,ASCII码中,字符和数字相差48
  46. mul rcx;乘法,乘以10
  47. add rax, rbx;
  48. inc rsi;增加rsi,即变为下一个字节。
  49. jmp next;调到函数next
  50.  
  51. return_str:
  52. ret;直接返回。
  53.  
  54. int_to_str:
  55. mov rdx, 0
  56. mov rbx, 10
  57. div rbx;除以10,获取个位数余数在rdx,商在rax。
  58. add rdx, 48
  59. add rdx, 0x0
  60. push rdx
  61. inc r12;首次迭代为0,记录字符的个数,用于后续输出。
  62. cmp rax, 0x0;商是否为0,为0则退出,跳转到print进行输出。
  63. jne int_to_str;商不为0,则继续输出。
  64. jmp print
  65.  
  66. print:
  67. mov rax, 1
  68. mul r12
  69. mov r12, 8
  70. mul r12
  71. mov rdx, rax
  72.  
  73. mov rax, SYS_WRITE
  74. mov rdi, STD_IN
  75. mov rsi, rsp
  76. ;; call sys_write
  77. syscall;调用sys_write输出结果
  78. jmp exit
  79.  
  80. exit:
  81. mov rax, SYS_EXIT
  82. mov rdi, EXIT_CODE
  83. 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汇编示例一

示例——退出

 

最简单的汇编推出示例如下,

 
  1.  
  2. .section .data
  3.  
  4. .section .text
  5. .globl _start
  6. _start:
  7.  
  8. movl $1, %eax # 退出程序的调用码
  9. movl $0, %ebx #返回给操作系统的状态
  10.  
  11. # 调用内核执行退出代码
  12. int $0x80
  13.  
 

进行汇编,得到对象文件。

#as exit.s -o exit.o

然后通过连接器将对象文件放在一起并加入信息,这样内核知道如何加载和运行。

# ld exit.o -o exit

然后执行./exit,执行完毕后执行

#echo $?

输出结果。

第7部分-Linux x86 64位汇编 AT&T汇编示例二

示例——找出最大值

代码max.s如下:

 
  1. # %edi - 被检查数据条目的索引
  2. # %ebx – 最大数据条目
  3. # %eax – 当前数据条目
  4. # # The following memory locations are used:
  5. # # data_items – 保持数据条目. 0用于接收数据条目
  6.  
  7. .section .data
  8. data_items: #数据
  9. .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
  10.  
  11. .section .text
  12. .globl _start
  13. _start:
  14.  
  15. movl $0, %edi #移动0到索引寄存器
  16. movl data_items(,%edi,4), %eax #加载第一个数据
  17. movl %eax, %ebx # 第一个数据为最大
  18.  
  19. start_loop: # 启动循环
  20.  
  21. cmpl $0, %eax # 确认是否到最后一个数据
  22. je loop_exit
  23. incl %edi # 加载到下一个值
  24. movl data_items(,%edi,4), %eax
  25. cmpl %ebx, %eax # 对比值
  26. jle start_loop # 是否大于原最大值
  27. movl %eax, %ebx # 更新最大值
  28.  
  29. jmp start_loop
  30.  
  31. loop_exit:
  32.  
  33. movl $1, %eax # exit() 系统调用
  34. int $0x80
  35.  
 

编译:as max.s -o max.o

链接:ld -o max max.o

执行./max, 然后通过#echo $? 进行输出结果。

第8部分-Linux x86 64位汇编 AT&T汇编示例三

示例——求指数

这里几个例子还是使用的32位的寄存器,后面会渐渐过渡到64位的寄存器。

在求指数中,我们定义了函数,通过函数来实现指数求解(这里实现的23+52=33,将参数写入了代码中)。

 
  1. .section .data
  2.  
  3. .section .text
  4.  
  5. .code32
  6. .globl _start
  7. _start:
  8.  
  9. pushl $3 #压栈第二个参数
  10. pushl $2 #压栈第一个参数
  11. call power #调用函数
  12. addl $8, %esp #移动栈指针
  13.  
  14. pushl %eax #保存第一个结果
  15.  
  16. pushl $2 #压栈第二参数
  17. pushl $5 ##压栈第一个参数
  18. call power #调用函数
  19. addl $8, %esp #移动栈指针
  20.  
  21. popl %ebx #第二个值在%eax中,第一个值在栈中
  22.  
  23. addl %eax, %ebx #将他们相加在%ebx
  24.  
  25. movl $1, %eax #退出
  26. int $0x80
  27.  
  28. #INPUT: 第一个参数-基数 %ebx
  29. # 第二个参数-指数 %ecx
  30. # -4(%ebp) 保存当前结果
  31. # %eax 用于临时存储
  32. #OUTPUT:
  33. # 结果返回
  34.  
  35. .type power, @function
  36. power:
  37.  
  38. pushl %ebp #保存旧的基指针
  39. movl %esp, %ebp #将栈指针移动给基指针
  40. subl $4, %esp #为本地变量腾出空间
  41.  
  42. movl 8(%ebp),%ebx #第一个参数放入到%eax
  43. movl 12(%ebp),%ecx #第二个参数放入到%ecx
  44.  
  45. movl %ebx, -4(%ebp) #存储当前结果
  46.  
  47. power_loop_start:
  48.  
  49. cmpl $1, %ecx #如果结果 是1,则结束
  50. je end_power
  51. movl -4(%ebp), %eax #移动当前结果到%eax
  52. imull %ebx, %eax #用于基数乘以当前结果
  53. movl %eax, -4(%ebp) #存储结果
  54.  
  55. decl %ecx
  56. jmp power_loop_start
  57.  
  58. end_power:
  59. movl -4(%ebp), %eax
  60. movl %ebp, %esp
  61. popl %ebp
  62. ret
  63.  
 

#as -o power.o power.s --32

# ld -o power power.o -melf_i386

#./power

通过echo $?可以得到33.

第9部分-Linux x86 64位汇编 AT&T汇编示例四

 

示例——输出字符串

如下代码示例:

 
  1. .data # 数据段声明
  2. msg : .string "Hello, world!\\n" # 要输出的字符串
  3. len = . - msg # 字串长度
  4. .text # 代码段声明
  5. .global _start # 指定入口函数
  6.  
  7. _start: # 在屏幕上显示一个字符串
  8. movl $len, %edx # 参数三:字符串长度
  9. movl $msg, %ecx # 参数二:要显示的字符串
  10. movl $1, %ebx # 参数一:文件描述符(stdout)
  11. movl $4, %eax # 系统调用号(sys_write)
  12. int $0x80 # 调用内核功能
  13.  
  14. # 退出程序
  15. movl $0,%ebx # 参数一:退出代码
  16. movl $1,%eax # 系统调用号(sys_exit)
  17. int $0x80 # 调用内核功能
  18.  
 

通过as汇编器进行编译。

# as -o hello.o hello.s

#ld -s -o hello hello.o

其中-s参数表示在链接时候去掉函数符号。

第10部分-Linux x86 64位汇编 函数调用规则

汇编语言程序中创建函数需要3个步骤。

  1. 定义需要的输入值
  2. 定义对输入值执行的操作
  3. 定义如何生成输出值以及如何把输出值传递给发出调用的程序。

定义输入值

可以使用寄存器,全局变量或者堆栈。

使用寄存器时候要注意,如果被调用的函数修改主程序使用的寄存器,那么在被调用之前保存寄存器的当前状态,并且在函数返回之后恢复寄存器的状态。可以使用PUSH和POP,或者PUSHA和POPA。

     全局变量是在内存中的位置,程序中所有函数都可以访问这些内存位置。

定义函数处理

汇编器中声明函数名称如下:

.type func1,&function

func1:

函数结束由RET指令定义,执行到RET指令时,程序控制返回主程序,返回的位置是紧跟在调用函数的CALL指令后面的指令。

和高级语言不通,可以把任意数量的函数代码放在_start之前也可以放在主程序之后。

 

定义输出值

和输入值类似,输出结果可以是下面两种常见的

把结果存放在一个或多个寄存器中

把结果存放在全局变量内存位置中。

 

访问函数

创建好函数后,就可以在程序中的任何位置进行访问。使用CALL指令用于把控制从主程序传递到函数。

call function

第11部分-Linux x86 64位汇编 函数完整示例

调用函数示例

这里开始使用到64位的寄存器了,编译的也是64位可执行程序了。

 
  1. .extern printf ;//调用外部的printf函数
  2. .section .data
  3.  
  4. result:
  5. .ascii "Area result is %f.\n"
  6. .byte 0x0a,0x0
  7.  
  8. precision:
  9. .byte 0x7f, 0x00
  10. .section .bss
  11. .lcomm value, 4
  12. .lcomm areasize, 8
  13.  
  14. .section .text
  15. .globl _start
  16. _start:
  17. nop
  18. finit;//初始化FPU
  19. fldcw precision;//加载精度到FPU的控制寄存器
  20.  
  21. movl $10, %ebx
  22. call area;//调用函数area
  23. movq $result,%rdi
  24. fstpl areasize
  25. movq areasize, %xmm0
  26. call printf
  27.  
  28.  
  29. movl $2, %ebx
  30. call area;//调用函数area
  31. movq $result,%rdi
  32. fstpl areasize
  33. movq areasize,%xmm0
  34. call printf
  35.  
  36.  
  37. movl $120, %ebx
  38. call area;//调用函数area
  39. movq $result,%rdi
  40. fstpl areasize
  41. movq areasize,%xmm0
  42. call printf
  43.  
  44. mov $60,%rax
  45. syscall
  46.  
  47. .type area, @function;//定义函数area
  48. area:
  49. fldpi;//加载π
  50. imull %ebx, %ebx;//半径相乘
  51. movl %ebx, value;//半径相乘结果移动到value
  52. filds value;//加载半径相乘结果到st0,π移动到st1
  53. fmulp %st(0), %st(1) ;//求面积,保存到栈顶st0
  54. ret
 

as -o functest.o functest.s

ld -o functest functest.o -lc -I /lib64/ld-linux-x86-64.so.2

汇编语言||基本传送指令MOV的用法详解

MOV指令

MOV指令,能实现以下操作:

  1. CPU内部寄存器之间数据的任意传送(除了码段寄存器CS和指令指针IP以外)。
  2. 立即数传送至CPU内部的通用寄存器组(即AX、BX、CX、DX、BP、SP、SI、DI),给这些寄存器赋初值。
  3. CPU内部寄存器(除了CS和IP以外)与存储器(所有寻址方式)之间的数据传送,可以实现一个字节或一个字的传送。
  4. 能实现用立即数给存储单元赋初值。  

其中:

所以,注意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样式要求参数存放到堆栈中的顺序和函数的原型中的顺序相反。

 
  1. .extern printf ;//调用外部的printf函数
  2. .section .data
  3. precision:
  4. .byte 0x7f, 0x00
  5. resultstr:
  6. .ascii "Area result is %f.\n"
  7.  
  8. .section .bss
  9. .lcomm bresult, 4
  10.  
  11. .section .text
  12. .globl _start
  13. _start:
  14. nop
  15. finit;//FPU初始化
  16. fldcw precision;//加载FPU控制器
  17. push $10;//加载10到栈中
  18. call area;//调用area函数
  19. addq $8, %rsp;//增加8个字节,即跳过压栈的参数
  20. movl %eax, bresult;//保存结果到result
  21.  
  22. movq bresult,%xmm0;//结果复制到xmm0寄存器
  23. cvtps2pd %xmm0,%xmm0;//转化单精度为双精度
  24. movq $resultstr,%rdi
  25. call printf
  26.  
  27. push $2
  28. call area
  29. addq $8, %rsp
  30. movl %eax, bresult
  31. movq $resultstr,%rdi
  32. movq bresult,%xmm0
  33. cvtps2pd %xmm0,%xmm0
  34. call printf
  35.  
  36. push $120
  37. call area
  38. addq $8, %rsp
  39. movl %eax, bresult
  40. movq $resultstr,%rdi
  41. movq bresult,%xmm0
  42. cvtps2pd %xmm0,%xmm0
  43. call printf
  44.  
  45. mov $60,%rax
  46. syscall
  47.  
  48. .type area, @function;//定义函数area
  49. area:
  50. push %rbp;//压栈rbp寄存器
  51. mov %rsp, %rbp;//将rsp赋值为rbp,,rsp <- rbp
  52. subq $8, %rsp;//设置rsp网上走,这里其实没有实际作用,64位系统中一个压栈就是8个字节
  53. fldpi;//加载π到st0, FLD是Intel的指令集协处理器的指令,用于把浮点数字传送入和传送出FPU寄存器。
  54. filds 16(%rbp) ;//加载rbp+8就是调用函数前压栈的参数到st0,π移到st1,fild是将整数转化为长双精FP80压栈(压到st0) fstp是将弹栈指令,将st0弹出
  55. fmul %st(0), %st(0) ;//st0和st0相乘,保存在st0
  56. fmulp %st(0), %st(1) ;//st0和st1相乘,结果在栈顶st0
  57. fstps -8(%rbp) ;//保存结果到栈中, 8个字节,也就是挡墙rsp所指的位置。
  58. movl -8(%rbp), %eax;//最后将结果移动到eax寄存器,是单精度值,汇编指令 movmovb (8位)、movw (16位)、 movl (32位)、movq (64位)
  59.  
  60. mov %rbp, %rsp;//恢复esp寄存器(->ebp)
  61. pop %rbp;//恢复ebp寄存器
  62. ret;//返回函数
 

as -g -o ccalltest.o ccalltest.s

ld -o ccalltest ccalltest.o -lc -I /lib64/ld-linux-x86-64.so.2

 

 

posted @ 2023-04-06 15:35  CharyGao  阅读(719)  评论(0编辑  收藏  举报