X86处理器汇编技术系列3

第21部分- Linux x86 64位汇编 字符串传送MOVS

MOVS指令

MOVS指令把字符串从一个内存位置传送到另一个内存位置。

MOVS有多种格式:MOVSB,MOVSW,MOVSQ

MOVS指令使用隐含的源和目标操作数。源操作数是ESI寄存器,指向源字符串,目标操作数是EDI寄存器,指向字符串要被复制到的目标内存位置。每次数据传送后,ESI和EDI寄存器会自动改变。可以自动递增也可以自动递减,取决于EFLAGS寄存器的DF表示。

CLD用于将DF标志清零。

STD用于设置DF标志位。

可以通过movl 将内存地址加载到edi寄存器中个,也可以使用lea指令。

例如:

leal output,%edi

复制大型字符串时候,可以通过ECX来进行字符串长度控制。

传送数字示例

 
  1. .extern printf ;//调用外部的printf函数
  2. .section .data
  3. value1:
  4. .ascii "This is a test string.\n"
  5. .section .bss
  6. .lcomm output, 23
  7. .section .text
  8. .globl _start
  9. _start:
  10. nop
  11. leal value1, %esi;//获取value1地址
  12. leal output, %edi;//获取output地址
  13. movl $23, %ecx
  14. cld
  15. loop1:
  16. movsb
  17. loop loop1
  18.  
  19. mov $output,%rdi
  20. movq $23,%rsi
  21. call printf
  22.  
  23. mov $60,%rax
  24. syscall
 

as -g -o movstest.o movstest.s

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

GNUx86-64汇编

寄存器

X86-64大约有16个64位整数寄存器,其中栈指针rsp和基址指针rbp较为特殊,rsirdi跟处理字符串相关。后面的八个寄存器是编号的,使用起来没有特殊限制。

  • rax rbx rcx rdx
  • rsi rdi rbp rsp
  • r8 - r15

其中rax的结构如下
[image:ECA803C6-AECB-4593-8CAA-34CF915FBC41-86030-0001242002E5C3CA/20171008192750274.png]
rax的低八位为al,接着八位是ah,合并为ax,低32位为eax,整个64位是rax

R8的结构如下
[image:8D73982F-FE1D-4750-ADD6-9CDBEC733828-86030-0001243A3CCBAFA8/20171008193142962.png]
大多数编译器产品会混合使用32位和64位模式。32位用来做整数计算,64位一般用来保存内存地址(指针)。

寻址模式

mov指令有一个决定移动多大数据的单字母前缀

  • movb Byte 8bits
  • movw Word 16bits
  • movl Long 32bits
  • movq Quadword 64bits

直接寻址

不同的数据有不同的寻址模式
全局值和函数:直接使用名字,如printf
常数:带有美元符号的立即数,如$56
寄存器:使用寄存器名称,如%rbx

间接寻址

简介寻址是使用与寄存器保存的地址对应的内存中的值,如(%rsp)表示rsp寄存器指向的内存中的值。

相对基址寻址

表示把一个常数加到寄存器值上,例如-16(%rcx)表示把rcx指向的地址前移16个字节后对应的内存值。

寻址模式相对于管理栈空间、局部变量、函数参数很重要,相对基址寻址也有很多变种,例如-16(%rbx, %rcx, 8)表示-16+%rbx+%rcx*8对应的地址的内存值,这种寻址模式在访问元素大小特殊的数组时很有用。
下面都表示将一个值加载到rax寄存器上
[image:676C703E-273B-45BF-8548-7A17272C9F8E-86030-000124D32C54E673/20171008200403991.png]

计算

编译器会用到四个基本算数计算指令

  • ADD
  • SUB
  • IDIV
  • IMUL
    上面的三个操作都有两个操作数,目的操作数在操作以后会被改写。

ADDQ %rbx, %rax
表示rbx的值加上rax的值,写到rax内。在例如写b=b*(b+a)的时候需要注意不要把b的值覆盖了,如下

movq a, %rax
movq b, %rbx
addq %rbx, %rax
imulq %rbx
movq %rax, c

IMUL操作只有一个操作数,表示把%rax的值乘以操作数,把低64位放在%rax,高64位放在%rdx。IDIV相反,把低64位的%rax,高64位的%rdx表示的数除以操作数,商放在%rax,余数在%rdx。cdqo指令会把%rax符号扩展到%rdx

movq a, %rax
cdqo
idivq $5    # divide %rdx:%rax by 5, leaving result in %eax

INC和DEC会把寄存器的值破坏掉。例如,语句a=++b可以这样翻译:

movq b, %rax
incq %rax
movq %rax, a

布尔操作的工作方式类似,AND,OR,XOR,NOT也会破坏寄存器的值。

小贴士: 浮点数 
我们不讨论浮点数操作细节,只需要知道它们使用一套不同的指令和寄存器。在老式机器上,浮点指令是使用可选的外部8087 FPU处理的,所以被称作X87操作,虽然现在已经集成到了CPU里面。X87 FPU包含 
8个排列在栈中的80位寄存器(R0-R7)。做浮点算术前,代码必须先把数据push到FPU栈,然后操作栈顶的数据,并回写到内存。内存中双精度浮点数是以64位的长度存储的。这种架构的一个奇怪的地方是,FPU的精度是80位,比内存中的存储方式精度高。结果,浮点计算的值会改变,取决于数据在内存和寄存器之间移动的具体顺序。 
浮点数数学计算比它看上去要难懂,推荐阅读: 
1. Intel 手册8-1章节。 
2. 计算机科学家必知之浮点数 
3. 程序员必知之浮点数
--------------------- 
作者:阿威_t 
来源:CSDN 
原文:https://blog.csdn.net/pro_technician/article/details/78173777 

比较和跳转

JMP指令可以构造一个无限循环,%eax开始计数

    movq $0, %rax
loop:
    incq %rax
    jump loop

所有的比较都用CMP指令,指令比较两个不同的寄存器中的值,设置eflag寄存器的比特位,记录下结果,jump指令集会利用eflag寄存器中的结果进行跳转
[image:A28DA285-C819-4485-BBCF-63999A382B76-86030-00012629E8112A97/20171013211645843.png]
下面是一个从0累加到5的循环

    movq $0, %rax
loop:
    incq %rax
    cmp $5, %rax
    jle loop

设置y的值,如果x大于0,y=10,否则为20

    movq x, %rax
    cmpq $0, %rax
    jle twenty
ten:
    movq $10, %rbx
    jmp done
twenty:
    movq $20, %rbx
    jmp done
done:
    movq %rbx, y

注意:上面的ten/twenty/done都是标签,标签在一个汇编文件中私有,对外部不可见,除非有.globl标志。c语言的说法,汇编中没有修饰的标签是static的,.globl修饰的标签是extern的。

堆栈

一般内存有如下结构

|----内存高位----|
|--------------|
|--------------|<-------栈底
|--------------|
|--------------|(栈空间向下增长)
|--------------|
|--------------|<-------栈顶
|--------------|
|--------------|
|--------------|<-------堆顶
|--------------|
|--------------|(堆空间向上增长)
|--------------|
|--------------|<-------堆底
|--------------|
|----内存低位----|

函数调用会将参数压入栈中,等调用完后再恢复栈结构,完成一次调用。
%rsp栈指针,指向栈顶,压栈的操作是将%rsp减去8字节,预留出64位,并把%rax写到%rsp指向的内存空间。

subq $8, %rsp
movq %rax, (%rsp)

等价于

pushq %rax

Pop刚好相反

movq (%rsp), %rax
addq $8, %rsp

等价于

popq %rax

如果想丢弃栈中的值,只需要增加%rsp的值

addq $8, %rsp

函数调用

X86-64的函数堆栈System V ABI较为复杂,这里只做简单的介绍

  • 整形参数(和指针)以此放在%rdi, %rsi, %rdx, %rcx, %8, %9寄存器中
  • 浮点参数依次放在%xmm0-%xmm7中
  • 寄存器不够用时,参数放在栈中
  • 可变参数(printf),寄存器%eax需要记录下有多少个浮点参数的个数
  • 被调用的函数可以使用任何寄存器,但必须保证%rbx, %rbp, %rsp和%r12-%15恢复到原来的值
  • 返回值放在%eax中
    [image:E96C7FBD-E6FF-405C-8372-3C74672A64E3-86030-00012E397428F584/20171015115531621.png]
    函数调用前,需要先把参数放到寄存器中,将%r10和%r11的值保存到栈中,之后执行call指令,把IP指针的值保存到栈中,然后跳转执行,从函数恢复后,恢复%r10和%r11的值,并从%eax中获取返回值。
long x=0;
long y=10;
int main()
{
    x = printf("value: %d", y);
}

对应的汇编

.data
x:
    .quad 0
y:
    .quad 10
str:
    .string "value: %d"

.text
.globl main
main:
    movq $str, %rdi
    movq y, %rsi
    movq $0, %eax #没有浮点数
    pushq %r10
    pushq %r11
    
    call printf
    
    popq %r11
    popq %r10
    
    movq %rax, x
    ret
long square(long x)
{
    return x*x;
}
.globl square
square:
    movq %rdi, %rax
    imulq %rdi, %rax
    ret

一个复杂函数的调用都有如下步骤

  1. 改变栈底值
  2. 将参数依次压入栈中
  3. 预留函数调用的local variables的空间
  4. 保护好原有的寄存器rbx, r12-r15
  5. 函数调用
  6. 恢复原有的寄存器
  7. 恢复栈底
.globl func
func:
    pushq %rbp          # save the base pointer
    movq  %rsp, %rbp    # set new base pointer

    pushq %rdi          # save first argument on the stack
    pushq %rsi          # save second argument on the stack
    pushq %rdx          # save third argument on the stack

    subq  $16, %rsp     # allocate two more local variables

    pushq %rbx          # save callee-saved registers
    pushq %r12
    pushq %r13
    pushq %r14
    pushq %r15

    ### body of function goes here ###

    popq %r15            # restore callee-saved registers
    popq %r14
    popq %r13
    popq %r12
    popq %rbx

    movq   %rbp, %rsp    # reset stack to previous base pointer
    popq   %rbp          # recover previous base pointer
    ret                  # return to the caller

%rbp和%rsp之间的内存缴存stack frame也叫做活动记录。
下面是func内部的栈内存布局。
[image:D59227A1-B882-4E26-B2E1-1926878C9833-86030-00012F66B9789F54/20171015133500035.png]
%rbp指明了栈帧的开始。在函数体内,我们可以用%rbp基址相对寻址方式来引用参数和局部变量。参数0在 -8(%rbp)位置,参数1在 -16(%rbp),以此类推。 -32(%rbp) 对应局部变量,-48(%rbp)对应保存的寄存器。%rsp指向栈中最后一个元素。如果栈还要另作他用,则需要向更低地址的区域压栈。(注意:我们假设所有参数和变量都是8字节长度, 实际上不同的类型的长度不一样,对应的偏移也不一样)。

下面是一个真实的汇编

#include <stdio.h>
int sum(int a, int b)
{
    return a+b;
}
int main()
{
    int x=10;
    int y=20;
    printf("sum is:%d\n", sum(x,y));
    return 0;
}
        .globl  __Z3sumii               ## -- Begin function _Z3sumii
__Z3sumii:                              ## @_Z3sumii
        .cfi_startproc
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    -4(%rbp), %esi
        addl    -8(%rbp), %esi
        movl    %esi, %eax
        popq    %rbp
        retq
        .cfi_endproc
                                        ## -- End function
        .globl  _main                   ## -- Begin function main
_main:                                  ## @main
        .cfi_startproc
        pushq   %rbp #保存栈底
        movq    %rsp, %rbp #将栈顶用作新的栈底,保存旧栈帧
        subq    $16, %rsp  #预留4个字节作为栈大小
        movl    $0, -4(%rbp) #0压栈,我也不知道为什么
        movl    $10, -8(%rbp)#两个变量压栈
        movl    $20, -12(%rbp)
        movl    -8(%rbp), %edi#将值写入edi/esi寄存器,准备调用
        movl    -12(%rbp), %esi
        callq   __Z3sumii
        leaq    L_.str(%rip), %rdi
        movl    %eax, %esi#记录返回值到esi
        movb    $0, %al
        callq   _printf
        xorl    %esi, %esi
        movl    %eax, -16(%rbp)#保存结果         ## 4-byte Spill
        movl    %esi, %eax
        addq    $16, %rsp #恢复栈顶
        popq    %rbp #恢复栈底
        retq
        .cfi_endproc
                                        ## -- End function
L_.str:                                 ## @.str
        .asciz  "sum is:%d\n"

深入浅出GNU X86-64 汇编 - pro_technician的专栏 - CSDN博客

第22部分- Linux x86 64位汇编 字符串传送REP

 

REP指令

REP指令本身不执行什么操作,指令可以按照特定次数重复执行字符串指令,由ECS寄存器中的值进行控制。类似循环,但是不需要额外的loop指令。

Rep指令重复执行紧跟其后的字符串指令,知道ECX寄存器中的值为0.

此外,还有一些REP指令如下:

 

其中REPE和REPZ同义词。

REPNE和REPNZ指令是同义词。

示例

 
  1. .extern printf ;//调用外部的printf函数
  2. .section .data
  3. value1:
  4. .ascii "This is a test string.\n"
  5. .section .bss
  6. .lcomm output, 24
  7. .section .text
  8. .globl _start
  9. _start:
  10. nop
  11. leal value1+22, %esi
  12. leal output+22, %edi
  13. movl $23, %ecx
  14. std
  15. rep movsb
  16.  
  17. cld;//这里要清理DF,不然输出函数printf会包core dump
  18. movq $value1,%rdi
  19. call printf
  20.  
  21. mov $60,%rax
  22. syscall
 

as -o reptest.o reptest.s

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

 

posted @ 2023-04-08 09:46  CharyGao  阅读(75)  评论(0编辑  收藏  举报