从汇编的视角理解C语言
本文将详细介绍汇编语言的编写方法同时将从汇编的视角深入理解C语言。目录一.寄存器和内存二.汇编程序基本格式及编译方式三.汇编语言基本指令四.流程控制指令五.编写和调用函数六.栈相关的指令七.构建数据结构一.寄存器和内存汇编语言本质就是机器码的助记符其作用就是操作寄存器和读写内存从而使得CPU按照指令运行。所以想要理解汇编语言必须了解寄存器和内存和指令三部分。1.寄存器寄存器就是CPU内部集成的用于处理暂存指令数据地址的高速存储单元。由于寄存器在CPU内部所以其运行速度最快。同时访问寄存器不需要地址要直接使用寄存器的名字。寄存器有一个叫位宽的概念。位宽就是寄存器处理器或存储单元等一次能并行处理传输或存储的二进制位数。而且我们常说的操作系统是多少位其实就是按照CPU的通用寄存器的位宽来划分的。也就是64位系统的寄存器最多能存64个bit32位16位也同理。这里要注意本文编写的汇编均基于64位的x86-64架构的Ubuntu Linux系统。同时采用GCC编译器默认的ATT汇编语法(当然除此之外还有Intel汇编语法)。下面介绍常用的寄存器及主要功能寄存器(64)对应32位主要功能raxeax系统调用号函数返回值算数运算(乘法结果低64位除法的商)rdiedi系统调用第一个参数函数第一个参数rsiesi系统调用第二个参数函数第二个参数rdxedx系统调用第三个参数函数第三个参数算术运算(乘法结果高64位除法的余数)rcxecx循环的计数器函数的第四个参数(注意系统调用的第四个参数使用r10)rspesp指向栈顶的指针由push/pop自动修改rbpebp指向栈底的指针r8 - r15r8d - r15d扩展寄存器存放函数的参数存放临时变量2.内存内存是程序用于临时存储数据指令中间结果的存储空间。CPU一般要通过地址访问其内容主要用于和寄存器配合使用。这里要注意本文所讲的内存其实是操作系统进程的虚拟内存空间并非硬件物理内存。在程序实际访问硬件时必须由CPU内部的MMU进行转换才能真正操作物理内存条。可以简单的把内存理解为一个极大的线性字节数组每一个字节都有一个唯一的编号这个编号就是内存地址。下面介绍内存主要分区及作用内存分区主要作用栈区(高地址存放函数局部变量、函数调用栈帧、返回地址、函数参数从高地址向低地址生长共享库 / 内存映射区用于加载动态链接库、系统库函数存放GOT/PLT表堆区由malloc/free等函数手动管理的动态内存从低地址向高地址生长数据段(.data/.rodata/.bss.data已初始化的可读写全局变量静态变量.rodata只读常量.bss未初始化的可读写全局变量静态变量程序启动时自动清零代码段(.text)低地址存放程序执行的机器指令只读可执行二.汇编程序基本格式及编译方式下面就是一段汇编语言的程序可以看到程序中有两个段分别是.data和.text就对应了上面说的内存的两个分区。然后.global _start是用于告知链接器程序是从_start开始运行的。这里要注意所有以 . 开头的指令叫做伪指令伪指令不生成CPU指令只是给汇编器和链接器看的。.global _start .data msg: .asciz Hello Assembly\n len . - msg .text _start: mov $1, %rax mov $1, %rdi mov $msg, %rsi mov $len, %rdx syscall mov $60, %rax mov $0, %rdi syscall下面是将程序汇编和链接的指令最后生成hello的可执行文件。#汇编 as hello.s -o hello.o #链接 ld hello.o -o hello下面重点讲讲伪指令伪指令主要负责管理代码怎么放数据怎么存储程序从哪里开始之类的。下面列出一些常用的伪指令.text/.data/.bss段定义伪指令主要告诉内容是代码还是变量应该放在内存哪个分区。.byte/.word/.long/.quad程序中声明一段固定数据这些伪指令的区别是数据的字节数。.ascii/.asciz定义ASCII字符串区别是.asciz字符串后面自动加\0而.ascii没有。.space size用于预留size字节大小的区域并全部置0。.fill count, size, value重复count次每次写size字节的value。.global将指定符号声明为全局可见常用于声明程序入口_start。.extern声明符号为外部定义的不在当前文件中需要再链接阶段再寻找。.org设置后续的指令或数据从该指定地址开始存放例如.org 0x7C00。.code16/.code32/.code64指定16/32/64位汇编模式。.align N让下一行代码或数据的地址自动对齐到 2^N 字节边界。三.汇编语言基本指令下面是汇编语言指令的基本格式其中操作数前要加上$寄存器前要加上%。指令 源操作数,目标操作数基础指令可以大概分为三部分分别是数据传送类算数运算类逻辑运算类。数据传送类主要是MOV系列指令用于将源操作数的值赋值给目标操作数这里要注意MOV指令传送的字节数。指令名对应C语言类型功能及示例movbchar传送1字节 movb %al, %blmovwshort传送2字节 movw %ax, %bxmovlint传送4字节 movl %eax, %ebxmovqlong指针传送8字节 movq %rax, %rbx然后就是算数运算类和逻辑运算类主要是addsubmuldiv分别对应C语言的-*\。 inc对应自加(a)dec对应自减(a--)。andorxornot分别对应|^~。甚至还有左移和右移几乎和C语言的运算符一一对应。下面看一段除法的例子用于计算20/3。.global _start .data .text _start: movq $20, %rax xorq %rdx, %rdx movq $3, %rbx divq %rbx movq $60, %rax movq $0, %rdi syscall当然基本指令中还有空指令nop相当于C语言的空语句。一般用于延时占位等待扩展。同时也常用于对齐指令地址或者稳定硬件。而且该指令在破解和逆向中也很常用。四.流程控制指令下面要介绍流程控制指令主要用于实现C语言的ifelsewhileforswitchgoto等关键字的功能。首先是最常用的无条件跳转指令jmp要配合标签使用。对应C语言的goto语句。下面的代码运行到jmp指令时会跳过_start后面的代码直接运行skip下面的指令最后只会打印print。.global _start .data info: .asciz print\n len_info . - info msg: .asciz jump\n len_msg . - msg .text _start: mov $1, %rax mov $1, %rdi mov $info, %rsi mov $len_info, %rdx syscall jmp skip mov $1, %rax mov $1, %rdi mov $msg, %rsi mov $len_msg, %rdx syscall skip: mov $60, %rax mov $0, %rdi syscall下面介绍比较指令cmp和条件跳转指令。cmp指令会用目标操作数减源操作数条件跳转指令有好多包括jejnejbjajbejae等。这两种指令一般配合使用先对操作数进行比较然后根据比较结果进行跳转。相当于C语言的ifelseswitch等。下面是一段两个操作数比较大小的汇编代码最后输出结果。.global _start .data msg1: .asciz greater\n len1 . - msg1 msg2: .asciz less\n len2 . - msg2 .text _start: mov $10,%rax cmp $5,%rax jg greater mov $1,%rax mov $1, %rdi mov $msg2, %rsi mov $len2, %rdx syscall jmp exit greater: mov $1,%rax mov $1, %rdi mov $msg1, %rsi mov $len1, %rdx syscall exit: mov $60, %rax syscall下面是使用比较指令和跳转指令编写的循环逻辑相当于C语言的dowhile和for语句。这里使用了循环计数寄存器rcx程序循环了10次。.global _start .text _start: mov $0,%rcx loop: inc %rcx cmp $10,%rcx jl loop mov $60,%rax mov $0, %rdi syscall五.编写和调用函数函数主要分为两种一是系统调用二是自定义函数。系统函数的底层是一个系统调用表每一个系统函数都对应一个调用号调用号要输入rax中系统调用参数的寄存器分别是rdirsirdxr10r8r9最后函数返回值传送给rax然后用系统调用指令syscall进行执行即可。下面是代码示例这段代码用fork函数创建了两个进程然后分别打印两个字符串表示父进程和子进程。.global _start .data msgp: .asciz I am parent\n lenp . - msgp msgc: .asciz I am child\n lenc . - msgc .text _start: mov $57, %rax syscall cmp $0, %rax je child parent: mov $1,%rax mov $1, %rdi mov $msgp, %rsi mov $lenp, %rdx syscall movq $60, %rax movq $1, %rdi syscall child: mov $1,%rax mov $1, %rdi mov $msgc, %rsi mov $lenc, %rdx syscall movq $60, %rax movq $0, %rdi syscall下面讲解下自定义函数的写法。这里有两个重要的指令一是函数返回指令ret相当于C语言的return语句用于函数返回调用它的程序。另一个就是调用自定义函数指令call后面加上想要调用的函数段的标签即可下面的程序就是定义一个将rdi和rsi的值相加的函数然后调用的示例。.global _start .text func: add %rdi,%rsi mov %rsi,%rax ret _start: mov $10,%rdi mov $20,%rsi call func mov $60,%rax mov $0,%rdi syscall六.栈相关的指令栈是整个汇编语言和C语言运行时最重要的结构。本质来说栈就是一段内存专门用来支持函数调用和局部变量存储遵循严格的先进后出的规矩。有两个专门的寄存器用来控制栈分别是rsp和rbp。rbp指向栈底位于高地址rsp指向栈顶位于低地址。也就是说栈是从高地址向低地址生长的当有值入栈时rsp会变小相反有值出栈rsp会变大。下面是栈的示意图。下面是一段栈相关的代码示例展示了基础的入栈和出栈操作其中push指令将rax寄存器里的操作数放入栈中,也就是rsp当前指向的内存地址然后pop指令将rsp当前指向的内存地址里的操作数放入rax中,也就是出栈。.global _start .text _start: mov $100,%rax push %rax pop %rax mov $60, %rax mov $0, %rdi syscall当然在C语言中函数栈是编译器自动管理的但是汇编语言中栈是由编写者自己控制的。首先要进行栈初始化要将之前函数的rbp压入栈中存起来同时将上一个函数使用的rsp复制给这个新函数的rbp这样栈可以接着用。然后让栈顶指针rsp减去8这样就开辟了一个栈空间用于存储操作数然后就压要将操作数压入栈。这里使用了基址偏移寻址法强行将rbp赋值给rsp,将rsp拉回来8字节,用于释放栈地址最后将刚才存储的上一个函数的rbp取出返回之前的函数。.global _start .text func: push %rbp mov %rsp, %rbp sub $8, %rsp movl $100, -8(%rbp) mov %rbp, %rsp pop %rbp ret _start: call func mov $60, %rax mov $0, %rdi syscall这里要着重说下基址偏移寻址法基本语法格式如下。地址计算的方式有效地址 基址寄存器地址 位移量位移(%基址寄存器)七.构建数据结构下面将介绍如何构建并使用数组和结构体。数组就是一块连续的内存其要求成员均是相同类型。下面是一段数组的基础代码示例。.global _start .data arr_int: .int 10,20,30,40,50 .text _start: mov arr_int,%eax mov arr_int4,%ebx mov arr_int8,%ecx mov $60, %rax mov $0, %rdi syscall正式因为数组的特殊结构数组经常使用基址变址寻址法用来提取不同的数组成员。下面重点介绍基址变址寻址法基本语法格式如下。位移量(%基址寄存器, %变址寄存器, 比例因子)有效地址 基址寄存器值 变址寄存器值 × 比例因子 偏移量这里的比例因子只能是1byte2byte4byte8byte 。下面是基址变址寻址法提取数组特定元素示例代码这段代码最后提取的数组元素是4。.global _start .data arr: .int 1,2,3,4,5 .text _start: mov $3,%rcx movl arr(,%rcx,4),%eax movq $60, %rax movq %rax, %rdi syscall下面介绍结构体的实现方式。结构体同样也存储在一块连续地址中但是每个元素所占的字节数并不相同所以需要预先写好每一个元素相对结构体首地址的偏移量。下面就是一段结构体代码。.global _start .data age 0 id 4 score 12 stu: .int 20 .quad 12345 .int 90 .text _start: movl stu(age), %eax movq stu(id), %rbx movl stu(score), %ecx movl $21, stu(age) movl $99, stu(score) mov $60, %rax mov $0, %rdi syscall在上面的代码中使用直接偏移寻址法直接寻址法格式如下。有效地址 变量的起始地址 偏移量变量名(偏移量) 变量名 偏移量 #也可以使用这种本人新手如有问题欢迎各位大佬指正。