1

硬件相关知识

 3 years ago
source link: https://www.ascotbe.com/2018/12/05/HardwareRelatedKnowledge/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

对两年前的知识进行了下温习,发现好多坑都填上了,然后把之前的文章删了总结到了一篇,到时候还有些点需要填上

Windows/Linux的内存结构

首先会把EBP压入栈中,然后把ESP的值保存到EBP中,接着ESP会像低地址增加,EBP保持在栈底不动(高地址),所有运算结束后弹出所有的值ESP会到和EBP相同的值(大部分是这样的)

Windows的内存结构分布

  • 首先是内存结构分布图,windows的内存默认是从0x80000000位置开始的

  • 栈的增长方向是从高地址到低地址递增的,用下面一个例子来解释

    • esp是栈指针,是cpu机制决定的,pushpop指令会自动调整esp的值;

      ebp只是存取某时刻的esp,这个时刻就是进入一个函数内后,cpu会将esp的值赋给ebp,此时就可以通过ebp对栈进行操作,比如获取函数参数,局部变量等,实际上使用esp也可以;

      假设执行print函数之前esp=Q

      main() {
      //执行test前
      print(int p1,int p2);
      //执行test后
      }
      #############开始执行print函数#############
      push p2; //函数参数p2入栈,esp=Q-4H
      push p1; //函数参数p1入栈,esp=Q-8H
      call print; //函数返回地址入栈,esp=Q-0CH
      #现在进入print内,做些准备工作:
      push ebp; //保护先前ebp指针,ebp入栈,esp=Q-10H
      mov ebp,esp; //设置ebp等于当前的esp
      #此时,ebp+0CH=Q-4H,即p2的位置
      #同样,ebp+08H=Q-8H,即p1的位置
      #下面是print内的一些操作:
      sub esp,20H; //设置长度为10H大小的局部变量空间,esp=Q-20H
      #... ...
      #一系列操作
      #... ...
      add esp,20H; //释放局部变量空间,esp=Q-10H
      pop ebp; //出栈,恢复原先的ebp的值,esp=Q-0CH
      ret 8; //ret返回,弹出先前入栈的返回地址,esp=Q-08H,后面加操作数8H为平衡堆栈
      #之后,弹出函数参数,esp=Q,恢复执行print函数前的堆栈;
      • 整个的流程图如下,要执行函数之前把p2p1压入栈中,栈向低地址递增(Q-4h),接着把函数地址放入栈中,然后把ebp的值放入栈中保存,下图中应该是ebp在address_return上面,图片有问题
    • EBP是当前函数的存取指针,即存储或者读取数时的指针基地址;ESP就是当前函数的栈顶指针。每一次发生函数的调用(主函数调用子函数)时,在被调用函数初始时,都会把当前函数(主函数)的EBP压栈,以便从子函数返回到主函数时可以获取EBP

      下面是按调用约定__stdcall 调用函数test(int p1,int p2)的汇编代码

      #假设执行函数前堆栈指针ESP为0xAAAAAAA ;EBP为0xAAAAAB0
      push p2 ;参数2入栈, ESP -= 4h , ESP = 0xAAAAAAA - 4h = 0xAAAAAA6
      push p1 ;参数1入栈, ESP -= 4h , ESP = 0xAAAAAAA - 8h = 0xAAAAAA2
      call test ;压入返回地址 ESP -= 4h, ESP = 0xAAAAAAA- 0Ch = 0xAAAAA9D,注意:这里是test函数的返回地址,即在代码段中的地址(偏移)。
      ;//进入函数内
      {
      push ebp ;保护先前EBP指针, EBP入栈(即0xAAAAAB0入栈,注意与返回地址区别), ESP-=4h, ESP = 0xAAAAA99
      mov ebp, esp ;设置EBP指针指向栈顶 0xAAAAA99
      mov eax, dword ptr [ebp+0ch] ;ebp+0ch为0xAAAAAA6即参数2的位置
      mov ebx, dword ptr [ebp+08h] ;ebp+08h为0xAAAAAA2,即参数1的位置
      sub esp, 8 ;局部变量所占空间ESP-=8, ESP = 0xAAAAA91
      ...
      add esp, 8 ;释放局部变量, ESP+=8, ESP = 0xAAAAA99
      pop ebp ;出栈,恢复EBP, ESP+=4, ESP = 0xAAAAA9D,即把栈中地址0xAAAAA99的内容pop到ebp中
      ret 8 ;ret返回,弹出返回地址,ESP+=4, ESP=0xAAAAAA2, 后面加操作数8为平衡堆栈,ESP+=8,ESP=0xAAAAAAA, 恢复进入函数前的堆栈.
      }

      原来ESP就是一直指向栈顶的指针,而EBP只是存取某时刻的栈顶指针,以方便对栈的操作,如获取函数参数、局部变量等。

  • 堆分为堆块和堆表

    • 引用大佬的图

    • 为了合理地组织堆区中的空闲堆块,提出了堆表的概念。堆表的数据结构决定了整个堆区的组织方式,一般位于堆区的起始位置,用于索引堆区中空闲堆块的重要信息,包括堆块的位置、大小、状态(空闲或占用)。

    • 传统内存统计单位往往是以字节位标准,但处于性能的考虑,堆内存按照大小不同组成不同的块,以堆块为单位进行标识。一个堆块包括两个部分:header部分和data部分。header是一个堆块头部的几个字节,用来标识这个堆块自身的信息。data是用来在最终分配给用户使用的数据区。

      • 堆块的分配可以分为三类,Lookaside分配、普通Freelist分配以及0Freelist(free[0])分配。

        Lookaside分配:寻找到大小匹配的空闲堆块 -> 修改状态为占用 -> 从堆表中解链 -> 给程序返回一个指向堆块的指针

        普通Freelist分配:寻找最优的空闲堆块 -> 若失败,寻找次优空闲堆块分配

        0Freelist分配:从free[0]反向寻找最后一个堆块(最大的堆块) -> 若满足要求,再正向搜索最小的满足要求的空闲堆块。

        堆块分配中的“找零钱”现象:当在Freelist中无法找到刚好合适的堆块时,此时会分配一个稍微大一点的空闲堆块给程序使用,其过程是首先在这个大块中分配出大小刚好等于请求堆块大小的堆块给程序,然后剩下的部分修改堆块的header信息,重新链入到Freelist合适的位置。这种方法节约了内存的使用,不会造成大量的内存浪费。

        由于Lookaside只有在精确匹配时才会分配,因此不存在“找零钱”现象。

      • 堆块的释放主要是将堆块修改为空闲状态,然后将堆块链入相应的堆表。所有的释放块都链入堆表的末尾,分配的时候也会首先从堆表末尾分配。

      • 为了减少内存中的内存碎片,合理有效地利用内存,堆管理系统还需要进行堆块合并操作。

        当两个空闲堆块彼此相邻的时候就会进行堆块合并操作。其过程大致为:

        将两个块从Freelist中解链 -> 合并堆块 -> 调整合并后堆块的header信息 -> 将合并后的堆块放入Freelist合适的位置

    • 堆上的漏洞

      • 堆溢出漏洞

        堆溢出与栈溢出在本质上是相通的,都是精心构造特制的数据去覆盖正常数据,覆盖到某个特定位置后跳转到自己的shellcode的地址去执行shellcode。
        但从技术层面来讲,堆溢出比栈溢出难度更大。而且现在基本很少有软件存在典型的栈溢出漏洞,相反由于堆的复杂性,很多软件仍然存在诸多的堆溢出漏洞。
      • UAF漏洞

        Use After Free(UAF),释放后重引用漏洞, 一块内存已经被释放后,在程序中仍然存在对该块内存的引用,并且在一定情况下可能使用内存中的数据。
        由于这块原本已经被释放不应该再使用的内存被程序中的其他地方进行了使用,因此该块内存中的数据是不可信的。这种方式甚至会造成内存崩溃或者任意代码执行。此类型的漏洞在浏览器中比较常见。
        UAF漏洞比较有名的是CVE-2013-1347 Microsoft IE CGenericElementUAF漏洞,该漏洞被用在了当时著名的“水坑”事件中,影响巨大。
      • Double Free漏洞

        双重释放漏洞,主要是由于对同一块内存进行二次重复释放。在释放过程中,邻近的已释放的堆块存在合并动作,这会导致原有的堆header信息发生改变,同时前向指针和后向指针也会发生改变,随后再对其中的地址进行引用,就会导致访问异常,最终导致程序崩溃或者任意代码执行。
        从另外一个角度来说,由于发生了对释放后的堆块内存的引用,因此Double Free漏洞也是UAF漏洞的一个子集。
        双重释放漏洞比较经典的是CVE-2014-1767,该漏洞位于Windows AFD.sys文件中。
        在2014年的Pwn2Own上,Siberas团队使用该漏洞进行内核提权,绕过了Windows 8.1平台上的IE11沙箱,并在随后获得了Pwnie Awards的“最佳提权漏洞奖”。该漏洞通杀Windows系统,影响较大。

Linux的内存结构分布

  • 首先依旧是先放个内存结构图,linux内存默认是从0xC0000000位置开始的

  • 栈地址也是依旧是高地址到低地址递增

  • 堆(待更新)

Windows/Linux的汇编区别

x86汇编一直存在两种不同的语法,在intel的官方文档中使用intel语法,Windows也使用intel语法,而UNIX平台的汇编器一直使用AT&T语法。而linux是UNIX衍生的一种系统所有也是使用AT&T语法。

相关区别:

  • AT&T使用$表示立即操作数,而Intel的立即操作数是不需要界定的。因此,使用AT&T语法引用十进制值4时,使用$4,使用Intel语法时只需使用4。
  • AT&T在寄存器名称前加上前缀%,而Intel不这样做。因此,使用AT&T语法引用EAX寄存器写为%eax。
  • AT&T语法处理源和目标操作数时使用相反的顺序。把十进制值4传送给EAX寄存器,AT&T的语法是movl $4, %eax,而Intel语法是mov eax, 4。
  • AT&T语法在助记符后面使用一个单独的字符来引用操作中使用的数据长度,而Intel语法中数据长度被声明为单独的操作数。AT&T的指令movl $test, %eax等同于Intel语法的mov eax, dword ptr test。
  • 长调用和跳转使用不同语法定义段和偏移值。AT&T语法使用ljmp $section, $offset,而Intel语法使用jmp section:offset。

用一张表就能解释:

AT&T风格 Intel风格 寄存器前加% 寄存器无需另加符号 立即数前加$ 立即数无需另加符号 16进制立即数使用0x前缀 16进制的立即数使用h后缀 源操作数在前,目的操作数在后(从前往后读) 目的操作数在前,源操作数在后(从后往前读) 间接寻址使用小括号() 间接寻址使用中括号[] 间接寻址完整格式:%sreg:disp(%base,index,scale) 间接寻址完整格式:sreg:[basereg + index*scale + disp] 操作位数:指令+l、w、b 指令+ dword ptr、word ptr、byte ptr

CPU架构

x86(IA-32)

x86是16位和32位处理器的统称。

x86泛指一系列英特尔公司用于开发处理器的指令集架构,这类处理器最早为1978年面市的intel 8086(并不是32位的处理器,而是16位微处理器,直到1985年32位的80386才是真正的32位处理器),由于之前上市的CPU都是以86结尾的,所以被称为x86,但是数字没办法注册称为商标,最后将其IA-32,全名为“Intel Architecture, 32-bit”。IA-32在第三代x86架构才使用上,也就是真正意义上的32位处理器

IA-64

IA-64,又称英特尔安腾架构(Intel Itanium architecture),使用在Itanium处理器家族上的64位元指令集架构,由英特尔与惠普共同开发。此架构与x86及x86-64并不相容,操作系统与软件需使用IA-64专用版本

第一款安腾于2001年推出,2017年推出最后一代安腾处理器,并停止开发。

x86-64(AMD64)

目前市面上除了英特尔安腾的64位CPU,其他Intel和AMD的支持64位的CPU都是使用这个架构的。

x86-64( 又称x64,即英文词64-bit extended,64位拓展 的简写)是x86架构的64位拓展,向后兼容于16位及32位的x86架构。x64于1999年由AMD设计,AMD首次公开64位集以扩展给x86,称为“AMD64”。其后也为英特尔所采用,现在英特尔称之为“Intel 64”,在之前曾使用过“Clackamas Technology” (CT)、“IA-32e”及“EM64T”来称呼。

其他各类叫法:

  • 苹果公司和RPM包管理员以“x86-64”或“x86_64”称呼此64位架构。

  • 甲骨文公司及Microsoft称之为“x64”。

  • BSD家族及其他Linux发行版则使用“amd64”,32位版本则称为“i386”(或 i486/586/686)。

  • Arch Linux用x86_64称呼此64位架构。

ARM架构,过去称作高级精简指令集机器(英语:Advanced RISC Machine),是一个精简指令集(RISC)处理器架构家族,其广泛地使用在许多嵌入式系统设计。常见的ARM架构CPU设备:树莓派(Broadcom)、手机(HiSilicon、Qualcomm)、大型工作站、超级计算机

架构 处理器家族 ARMv1 ARM1 ARMv2 ARM2、ARM3 ARMv3 ARM6、ARM7 ARMv4 StrongARM、ARM7TDMI、ARM9TDMI ARMv5 ARM7EJ、ARM9E、ARM10E、XScale ARMv6 ARM11、ARM Cortex-M ARMv7 ARM Cortex-A、ARM Cortex-M、ARM Cortex-R ARMv8 Cortex-A35、Cortex-A50系列、Cortex-A72、Cortex-A73

截止2020年CPU架构种类

PowerPC

PowerPC(英语:Performance Optimization With Enhanced RISC – Performance Computing,有时简称PPC)是一种基于精简指令集(RISC)的指令集架构 ISA(Instruction set architecture),其基本的设计源自IBM的POWER(Performance Optimized With Enhanced RISC)架构。PowerPC是1991年,Apple、IBM、Motorola组成的AIM联盟所发展出的微处理器架构。PowerPC是整个AIM联盟平台的一部分,并且是到目前为止唯一的一部分。但苹果电脑自2005年起,将旗下电脑产品转用Intel x86。

较广为人知的产品应用包含:

  • 苹果公司:Power Macintosh系列、PowerPC PowerBook系列(1995年以后的产品)、iBook系列、iMac系列(2005年以前的产品)、eMac系列产品。
  • 任天堂:GameCube、Wii和Wii U(之后换成了ARM架构处理器)。
  • 微软:Xbox 360(之后的产品换成了AMD的处理器)
  • 索尼:PlayStation 3(之后的产品换成了AMD的处理器)

MIPSMicroprocessor without Interlocked Pipeline Stages),是一种采取精简指令集(RISC)的指令集架构(ISA),1981年出现,由 MIPS 公司开发。最早的MIPS架构是32比特,最新的版本已经变成64比特。

MIPS 架构有多个版本。其中包括 MIPS I、II、III、IV,以及 MIPS V,早期的 MIPS 架构只有 32 位的版本,而其 64 位的版本随后才被开发。截至 2017 年 4 月,MIPS32/64 的当前版本是 MIPS32/64 Release 6。MIPS32/64 与

  • 北京君正集成电路股份有限公司:Xburst系列微架构半导体IP核
  • 中国龙芯系列产品

LoongISA

LoongISA(简称LISA)是龙芯中科注册的自主CPU指令集架构(由MIPS指令集拓展而来,采用MIPS的指令集格式)。LoongISA 指令集架构包括MIPS64 Release 2全套指令集和MIPS64 Release 5中的部分指令模块,以及其他一系列龙芯中科自主扩展的指令集。

SPARC

SPARC,名称源自于可扩展处理器架构Scalable Processor ARChitecture)的缩写,是一种RISC指令集架构,最早于1985年由Sun微系统所设计,也是SPARC国际公司的注册商标之一。

RISC-V

RISC-V(发音为“risk-five”)是一个基于精简指令集(RISC)原则的开源指令集架构(ISA),简易解释为开源软件运动相对应的一种“开源硬件”。该项目2010年始于加州大学柏克莱分校,但许多贡献者是该大学以外的志愿者和行业工作者。RISC-V指令集可以自由地用于任何目的,允许任何人设计、制造和销售RISC-V芯片和软件而不必支付给任何公司专利费。

X86架构汇编

Intel汇编和AT&T汇编的区别

  • Intel汇编
    • DOS、Windows,包括我们之前了解的8086处理器
    • Windwos派系:VC编译器
  • AT&T汇编
    • Linux、Unix、Mac OS、iOS模拟器
    • Unix派系:GCC编译器

以下使用的是Intel汇编语法讲解

32位CPU所含有的寄存器

  • 8个32位通用寄存器
    • 4个数据寄存器(EAX、EBX、ECX和EDX)
    • 2个变址和指针寄存器(ESI和EDI) 2个指针寄存器(ESP和EBP)
  • 6个段寄存器(ES、CS、SS、DS、FS和GS)
  • 1个指令指针寄存器(EIP)
  • 1个标志寄存器(EFlags)

每个寄存器都可作为一个32位值或两个16位值来寻址使用。某些16位的寄存器能够按照8位值寻址使用。

32 16 高8 低8 EAX AX AH AL EBX BX BH BL ECX CX CH CL EDX DX DH DL

下面几个没有8位模式:

32 16 ESI SI EDI DI EBP BP ESP SP

EAX的低16位称为AX,AX的高8位称为AH,低8位称为AL。

寄存器EAX存储了值0xA9DC81F5,代码可以使用其他三种方式来引用EAX中的这个数据:AX(2字节)是0x81F5,AL(1字节)是0xF5,AH(1字节)是0x81,如图下图所示

数据寄存器

数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器的时间。

32位CPU有4个32位的通用寄存器EAX、EBX、ECX和EDX。

  • 对低16位数据的存取,不会影响高16位的数据。
  • 寄存器EAX通常称为累加器(Accumulator),用累加器进行的操作可能需要更少时间。可用于乘、 除、输入/输出等操作,使用频率很高;
  • 寄存器EBX称为基地址寄存器(Base Register)。它可作为存储器指针来使用;
  • 寄存器ECX称为计数寄存器(Count Register)。 在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数;
  • 寄存器EDX称为数据寄存器(Data Register)。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址。

关于乘法和除法注意点:

  • 如果乘法的结果过大,会把结果分别存入EDX和EAX寄存器中,EDX存储高32位数据,EAX存储低32位数据

  • 如果要做除法之前,先要把除数赋值到EDX和EAX寄存器中,然后将结果存入EAX中,余数存入EDX中

指令 描述 mul 0x50 EAX值乘以0x50,并将结果存入EDX:EAX寄存器中 div 0x75 将EDX:EAX值除以0x75,并将结果存入EAX,将余数存入EDX
变址寄存器

32位CPU有2个32位通用寄存器ESI和EDI。

寄存器ESI、EDI、SI和DI称为变址寄存器(Index Register),它们主要用于存放存储单元在段内的偏移量, 它们可作一般的存储器指针使用。

指针寄存器

32位CPU有2个32位通用寄存器EBP和ESP。

它们主要用于访问堆栈内的存储单元,并且规定:

  • EBP为基指针(Base Pointer)寄存器,一般作为当前堆栈的最后单元,用它可直接存取堆栈中的数据;

  • ESP为堆栈指针(Stack Pointer)寄存器,用它只可访问栈顶。

指令指针寄存器

32位CPU把指令指针扩展到32位,并记作EIP

指令指针EIP、IP(Instruction Pointer)是存放下次将要执行的指令在代码段的偏移量。

标志寄存器

32位CPU的标志寄存器为EFLAG

CF:进位标志
OF:溢出标志
SF:符号标志
ZF:零标志
AC:辅助进位标志
PF:奇偶标志

以下使用的是AT&T汇编语法讲解

Intel用字(word)来表达16位数据类型,因此32位为双字(double words),64位为四字(quad words)

整数寄存器

X86-64中央处理单元(CPU)中有一组16个存储64位值的通用目的寄存器

初代8086中有816位的寄存器(%ax-%sp)

扩展带IA32架构时变成了832位寄存器(%eax-%esp)

扩展到X86-64架构时变成1664位寄存器(%rax-%rsp,%r8-%r15)前8个寄存器是做了扩充,后8个寄存器是新加的

16位操作可以访问最低的2个字节,32位操作可以访问最低的4个字节,而64位操作可以访问整个寄存器。

操作数指示符
普通的数据传送如图做了零扩展的数据传送做了符号扩展的数据传送

图中给了一个cltq指令,这条指令没有操作数:它总是以寄存器%eax作为源,%rax作为符号扩展结果为目的(它的效果与指令movslq %eax,%rax完全一致,不过编码更紧凑)

movb&movsbq&movzbq之间的差别:
movabsq   $0x0011223344556677,%rax                     # %rax=0011223344556677
movb $0xAA,%dl # %dl=AA
movb %dl,%al # %RAX=00112233445566AA
movsbq %dl,%rax # %rax=FFFFFFFFFFFFFFAA
movzbq %dl,%rax # %rax=00000000000000AA
#movb指令不改变其他字节
#movsbq将其他7个字节设为全1或全0,由于十六进制A表示二进制值1010,符号扩展会把高位字节都设置为FF
#movzbq讲其他7个字节都设置为0
压入和弹出栈数据

将一个四字值压人栈中,首先要将栈指针减8,然后将值写到新的栈顶地址。因此,指令pushq %rbp的行为等价于下面两条指令:

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

弹出一个四字的操作包括从栈顶位置读出数据,然后将栈指针加8。因此,指令popq %rax的行为等价于下面两条指令:

movq          (%rsp),%rax
addq $8,%rsp
算术和逻辑操作

这些操作被分成4组:加载有效地址,一元操作,二元操作和移位。二元操作有两个操作数,而一元操作有一个操作数。

加载有效地址

指令leaq实际上是movq指令的变形,它的指令形式是从内存读数据到寄存器,但实际上它根本没有引用内存。

假设寄存器%rax的值为X%rcx的值为y,结果为%rdx中的值

一元和二元操作

这里没有什么可以写的直接上例题吧

左移指令有两个名字:SALSHL。两者的效果都是一样的,都是将右边填上0

右移指令有两个名字:SARSHR。但是效果不同SAR执行算术移位(填上符号位),SHR执行逻辑移位(填上0)

特殊的算术操作
  • CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作数据的溢出。

  • ZF:零标志。最近的操作得出的结果为0.

  • SF:符号标志。最近的操作得到的结果为负数。

  • OF: 溢出标志。最近的操作导致一个补码溢出–正溢出或负溢出。

  • 比较和测试指令

    CMPTEST的区别:
    test逻辑与运算结果为零,就把ZF(零标志)置1
    cmp 算术减法运算结果为零,就把ZF(零标志)置1

  • 访问条件码

  • 直接跳转:跳转目标是作为指令的一部分编码。写法jmp .L1

    间接跳转:跳转目标是从寄存器或内存位置中读出。写法jmp *%rax或者jmp *(%rax)

    用寄存器%rax中的值作为跳转目标

    jmp *%rax
    %rax中的值作为读地址,从内存中读出跳转目标

    jmp *(%rax)

call指令有一个目标,指明被调用过程起始的指令地址。同跳转一样,也有直接和间接区别。直接调用的目标是一个标号,而间接调用的目标是*后面跟一个操作数指示符。

  • 是传送指令的变形,用法和跳转指令类似

指针类型不是机器代码的一部分
每个指针都是一个值,这个值是某个指定类型的对象地址。

后续在更新

关于数组,栈,一些像if,switch等书上讲的更详细

https://www.cnblogs.com/nufangrensheng/p/3893272.html
https://blog.csdn.net/lpwstr/article/details/78817831
《深入了解计算机系统》
https://wikipedia.org

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK