精华内容
下载资源
问答
  • 引用位和访问位
    万次阅读
    2022-05-06 22:11:15

    指针和引用的区别

    0、前言

    最近学习严老师的数据结构,发现很多函数声明时都会使用到引用,首先呢,我们要说的是,“引用”是C++中的概念,因为我没接触过C++,所以只能简单地看来看两者的区别。


    1、指针、指针变量和引用

    首先,我们先来看看指针、指针变量和引用的概念。

    指针:即内存地址。

    指针变量:存放内存地址的变量,即指针变量的值为指针。

    引用:某块内存的“别名”,给一个已经存在的变量起一个“别名”。

    注意:

    1. 不同类型的指针变量所占用存储空间是相同的,32位机用4字节存储地址,因此,32位的指针变量所占空间为4字节;64位则为8字节。
    2. 引用不是新定义了一个变量,而是给变量取“别名”,因此,引用变量和引用的变量共用一块内存空间,编译器并不会为引用变量开辟新的内存空间。

    2、指针变量和引用变量的定义

    指针变量的定义:

    int num = 18;
    int* p = #	//指针变量p的值就是num的内存地址
    

    引用变量的定义:

    //引用是C++中的概念!!!
    int num = 18;
    int &r = num;	//引用变量r就相当于num的“别名”
    

    注意:引用类型必须和引用实体是同种类型的,即变量是int型,则引用变量也必须是int型。

    这一点倒是和指针很相似,关于指针类型和变量类型不兼容的问题,可以看《C语言_地址与指针类型不兼容造成的影响》)。


    3、引用的用途

    引用的主要用途:修饰函数的形参和返回值。

    在C++中,函数的参数和返回值的传递方式有三种,分别为值传递、指针传递和引用传递。其中,引用具有指针的效率,又具有变量使用的方便性和直观性。

    实际上引用可以做的事,指针都可以做,那为何还需要“引用”呢?

    引用体现了最小特权原则,即给予程序元素完成其功能的最小权限,指针能够毫无约束的操作内存中的任何东西,功能很强大,但也很危险。


    4、引用和指针的区别

    上面说了,引用可以做的事,指针都可以做,但是两者还是存在一定区别的。

    1. 初始化:引用在定义时必须初始化;指针则没有要求(尽量初始化,防止野指针)。
    2. 引用在初始化引用一个实体后,就不能再引用其它实体;而指针可以在任意时候指向一个同类型实体。
    3. 没有NULL引用,但是有NULL指针(空指针)。
    4. 占用空间不同:使用sizeof()函数,引用结果为引用类型的大小;指针始终是地址空间的大小,即32位机4字节,64位机8字节。
    5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
    6. 有多级指针,但没有多级引用。
    7. 访问实体的方式不同,指针需要显式解引用,引用编译器帮我们处理。
    8. 引用比指针使用起来相对安全(只是相对,不是绝对)。

    5、引用的底层原理

    引用一般都是通过指针来实现的,只不过编译器帮我们完成了转换。

    我没学过汇编语言,但是我咨询了我的舍友大佬,他们通过编译器调试观察反汇编语言发现,引用和指针的反汇编语言是一样的,就是编译器帮我们完成了转换。

    底层实现:引用通过指针实现,定义一个引用类型的变量相当于定义于一个指针类型的变量。

    注意,引用是”别名“,不是指针,并没有发生拷贝,我们可以认为“引用”是“简单版的指针”。


    6、代码示例

    6.1、示例程序1

    #include "stdio.h"
    
    void modifyNumByPoint(int *p);
    
    void modifyNumByReference(int &r);
    
    int main() {
        int num01 = 18;
        int *p = &num01;
        printf("num01 = %d\n", num01);
        modifyNumByPoint(p);
        printf("modifyNumByPoint--num01 = %d\n", num01);
        printf("==========================\n");
        int num02 = 20;
        int &r = num02;
        printf("num02 = %d\n", num02);
        modifyNumByReference(r);
        printf("modifyNumByReference--num02 = %d\n", num02);
    
    }
    
    void modifyNumByPoint(int *p) {
        (*p)++;
    }
    
    void modifyNumByReference(int &r) {
        r++;
    }
    
    结果:
    num01 = 18
    modifyNumByPoint--num01 = 19
    ==========================
    num02 = 20
    modifyNumByReference--num02 = 21
    

    6.2、示例程序2

    如果是在子函数内修改主函数的一级指针,一般在数据结构中用得比较多,如链表等等,则可如下定义:

    //引用
    void modifyByReference(int *&){
        p = ……;
        ……
    }
    
    //指针
    void modifyByPoint(int **p){
        *p = ……;
        ……
    }
    

    注:如有错误,敬请指正!!!

    更多相关内容
  • C#访问SqLite的dll,分32位和64位两种

    热门讨论 2013-04-27 11:13:11
    C#访问SqLite的dll,分32位和64位两种
  • PE结构详解(64位和32位的差别)

    万次阅读 2018-02-27 18:05:32
    在大部分情况下,你都能写出同时适用于32位和64位PE文件的代码。 EXE文件与DLL文件的区别完全是语义上的。它们使用的是相同的PE格式。惟一的不同在于一个位,这个位用来指示文件应该作为EXE还是DLL。甚至DLL文件的...

    1 基本概念

    下表描述了贯穿于本文中的一些概念:

    名称 描述
    地址 是“虚拟地址”而不是“物理地址”。为什么不是“物理地址”呢?因为数据在内存的位置经常在变,这样可以节省内存开支、避开错误的内存位置等的优势。同时用户并不需要知道具体的“真实地址”,因为系统自己会为程序准备好内存空间的(只要内存足够大)
    镜像文件 包含以EXE文件为代表的“可执行文件”、以DLL文件为代表的“动态链接库”。为什么用“镜像”?这是因为他们常常被直接“复制”到内存,有“镜像”的某种意思。看来西方人挺有想象力的哦^0^
    RVA 英文全称Relatively Virtual Address。偏移(又称“相对虚拟地址”)。相对镜像基址的偏移。
    节是PE文件中代码或数据的基本单元。原则上讲,节只分为“代码节”和“数据节”。
    VA 英文全称Virtual Address。基址

    2 概览

    x86都是32位的,IA-64都是64位的。64位Windows需要做的只是修改PE格式的少数几个域。这种新的格式被称为PE32+。它并没有增加任何新域,仅从PE格式中删除了一个域。其余的改变就是简单地把某些域从32位扩展到64位。在大部分情况下,你都能写出同时适用于32位和64位PE文件的代码。

    EXE文件与DLL文件的区别完全是语义上的。它们使用的是相同的PE格式。惟一的不同在于一个位,这个位用来指示文件应该作为EXE还是DLL。甚至DLL文件的扩展名也完全也是人为的。你可以给DLL一个完全不同的扩展名,例如.OCX控件和控制面板小程序(.CPL)都是DLL。


    图1 解释了Microsoft PE可执行文件格式:

    PE文件总体上分为“头”和“节”。“头”是“节”的描述、简化、说明,“节”是“头”的具体化。

    3 文件头

    PE文件的头分为DOS头、NT头、节头。注意,这是本人的分法,在此之前并没有这种分法。这样分法会更加合理,更易理解。因为这三个部分正好构成SizeOfHeaders所指的范围,所以将它们合为“头”。这里的3个头与别的文章的头的定义会有所区别。

    节头紧跟在NT头后面。

    3.1 DOS头(PE文件签名的偏移地址就是大小)

    用记事本打开任何一个镜像文件,其头2个字节必为字符串“MZ”,这是Mark Zbikowski的姓名缩写,他是最初的MS-DOS设计者之一。然后是一些在MS-DOS下的一些参数,这些参数是在MS-DOS下运行该程序时要用到的。在这些参数的末尾也就是文件的偏移0x3C(第60字节)处是是一个4字节的PE文件签名的偏移地址。该地址有一个专用名称叫做“E_lfanew”。这个签名是“PE00”(字母“P”和“E”后跟着两个空字节)。紧跟着E_lfanew的是一个MS-DOS程序。那是一个运行于MS-DOS下的合法应用程序。当可执行文件(一般指exe、com文件)运行于MS-DOS下时,这个程序显示“This program cannot be run in DOS mode(此程序不能在DOS模式下运行)”这条消息。用户也可以自己更改该程序,有些还原软件就是这么干的。同时,有些程序既能运行于DOS又能运行于Windows下就是这个原因。Notepad.exe整个DOS头大小为224个字节,大部分不能在DOS下运行的Win32文件都是这个值。MS-DOS程序是可有可无的,如果你想使文件大小尽可能的小可以省掉MS-DOS程序,同时把前面的参数都清0。

    3.2 NT头(244或260个字节)

    紧跟着PE文件签名之后,是NT头。NT头分成3个部分,因为第2部分在32与64位系统里有区别,第3部分虽然也是头,但实际很不像“头”。

    第1部分(20个字节)

    偏移 大小 英文名 中文名 描述
    0 2 Machine 机器数 标识CPU的数字。参考3.2.1节“机器类型”。
    2 2 NumberOfSections 节数 节的数目。Windows加载器限制节的最大数目为96。
    4 4 TimeDateStamp 时间/日期标记 UTC时间1970年1月1日00:00起的总秒数的低32位,它指出文件何时被创建。
    8 8 已经废除
    16 2 SizeOfOptionalHeader 可选头大小 第2部分+第3部分的总大小。这个大小在32位和64位文件中是不同的。对于32位文件来说,它是224;对于64位文件来说,它是240。
    18 2 FillCharacteristics 文件特征值 指示文件属性的标志。参考3.2.2节“特征”。

    第2部分(96或112个字节)

    偏移 大小 英文名 中文名 描述
    0 2 Magic 魔数 这个无符号整数指出了镜像文件的状态。
    0x10B表明这是一个32位镜像文件。
    0x107表明这是一个ROM镜像。
    0x20B表明这是一个64位镜像文件。
    2 1 MajorLinkerVersion 链接器的主版本号 链接器的主版本号。
    3 1 MinorLinkerVersion 链接器的次版本号 链接器的次版本号。
    4 4 SizeOfCode 代码节大小 一般放在“.text”节里。如果有多个代码节的话,它是所有代码节的和。必须是FileAlignment的整数倍,是在文件里的大小。
    8 4 SizeOfInitializedData 已初始化数大小 一般放在“.data”节里。如果有多个这样的节话,它是所有这些节的和。必须是FileAlignment的整数倍,是在文件里的大小。
    12 4 SizeOfUninitializedData 未初始化数大小 一般放在“.bss”节里。如果有多个这样的节话,它是所有这些节的和。必须是FileAlignment的整数倍,是在文件里的大小。
    16 4 AddressOfEntryPoint 入口点 当可执行文件被加载进内存时其入口点RVA。对于一般程序镜像来说,它就是启动地址。为0则从ImageBase开始执行。对于dll文件是可选的。
    20 4 BaseOfCode 代码基址 当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍。
    24 4 BaseOfData 数据基址 当镜像被加载进内存时数据节的开头RVA。(在64位文件中此处被并入紧随其后的ImageBase中。)必须是SectionAlignment的整数倍。
    28/24 4/8 ImageBase 镜像基址 当加载进内存时镜像的第1个字节的首选地址。它必须是64K的倍数。DLL默认是10000000H。Windows CE 的EXE默认是00010000H。Windows 系列的EXE默认是00400000H。
    32 4 SectionAlignment 内存对齐 当加载进内存时节的对齐值(以字节计)。它必须≥FileAlignment。默认是相应系统的页面大小。
    36 4 FileAlignment 文件对齐 用来对齐镜像文件的节中的原始数据的对齐因子(以字节计)。它应该是界于512和64K之间的2的幂(包括这两个边界值)。默认是512。如果SectionAlignment小于相应系统的页面大小,那么FileAlignment必须与SectionAlignment相等。
    40 2 MajorOperatingSystemVersion 主系统的主版本号 操作系统的版本号可以从“我的电脑”→“帮助”里面看到,Windows XP是5.1。5是主版本号,1是次版本号
    42 2 MinorOperatingSystemVersion 主系统的次版本号  
    44 2 MajorImageVersion 镜像的主版本号  
    46 2 MinorImageVersion 镜像的次版本号  
    48 2 MajorSubsystemVersion 子系统的主版本号  
    50 2 MinorSubsystemVersion 子系统的次版本号  
    52 2 Win32VersionValue 保留,必须为0  
    56 4 SizeOfImage 镜像大小 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
    60 4 SizeOfHeaders 头大小 所有头的总大小,向上舍入为FileAlignment的倍数。可以以此值作为PE文件第一节的文件偏移量。
    64 4 CheckSum 校验和 镜像文件的校验和。计算校验和的算法被合并到了Imagehlp.DLL 中。以下程序在加载时被校验以确定其是否合法:所有的驱动程序、任何在引导时被加载的DLL以及加载进关键Windows进程中的DLL。
    68 2 Subsystem 子系统类型 运行此镜像所需的子系统。参考后面的“Windows子系统”部分。
    70 2 DllCharacteristics DLL标识 参考后面的“DLL特征”部分。
    72 4/8 SizeOfStackReserve 堆栈保留大小 最大大小。CPU的堆栈。默认是1MB。
    76/80 4/8 SizeOfStackCommit 堆栈提交大小 初始提交的堆栈大小。默认是4KB。
    80/88 4/8 SizeOfHeapReserve 堆保留大小 最大大小。编译器分配的。默认是1MB。
    84/96 4/8 SizeOfHeapCommit 堆栈交大小 初始提交的局部堆空间大小。默认是4KB。
    88/104 4 LoaderFlags 保留,必须为0  
    92/108 4 NumberOfRvaAndSizes 目录项数目 数据目录项的个数。由于以前发行的Windows NT的原因,它只能为16。

    第3部分数据目录(128个字节)

    偏移
    (PE32/PE32+)

    大小 英文名 描述
    96/112 8 Export Table 导出表的地址和大小。参考5.1节“.edata
    104/120 8 Import Table 导入目录表的地址和大小。参考5.2.1节“.idata
    112/128 8 Resource Table 资源表的地址和大小。参考5.6节“.rsrc
    120/136 8 Exception Table 异常表的地址和大小。参考5.3节“.pdata
    128/144 8 Certificate Table 属性证书表的地址和大小。参考6节“属性证书表
    136/152 8 Base Relocation Table 基址重定位表的地址和大小。参考5.4节“.reloc
    144/160 8 Debug 调试数据起始地址和大小。
    152/168 8 Architecture 保留,必须为0
    160/176 8 Global Ptr 将被存储在全局指针寄存器中的一个值的RVA。这个结构的Size域必须为0
    168/184 8 TLS Table 线程局部存储(TLS)表的地址和大小。
    176/192 8 Load Config Table 加载配置表的地址和大小。参考5.5节“加载配置结构
    184/200 8 Bound Import 绑定导入查找表的地址和大小。参考5.2.2节“导入查找表
    192/208 8 IAT 导入地址表的地址和大小。参考5.2.4节“导入地址表
    200/216 8 Delay Import Descriptor 延迟导入描述符的地址和大小。
    208/224 8 CLR Runtime Header CLR运行时头部的地址和大小。(已废除)
    216/232 8

    保留,必须为0

    3.2.1 机器类型

    Machine域可以取以下各值中的一个来指定CPU类型。镜像文件仅能运行于指定处理器或者能够模拟指定处理器的系统上。

    描述
    0x0 适用于任何类型处理器
    0x1d3 Matsushita AM33处理器
    0x8664 x64处理器
    0x1c0 ARM小尾处理器
    0xebc EFI字节码处理器
    0x14c Intel 386或后继处理器及其兼容处理器
    0x200 Intel Itanium处理器
    0x9041 Mitsubishi M32R小尾处理器
    0x266 MIPS16处理器
    0x366 带FPU的MIPS处理器
    0x466 带FPU的MIPS16处理器
    0x1f0 PowerPC小尾处理器
    0x1f1 带符点运算支持的PowerPC处理器
    0x166 MIPS小尾处理器
    0x1a2 Hitachi SH3处理器
    0x1a3 Hitachi SH3 DSP处理器
    0x1a6 Hitachi SH4处理器
    0x1a6 Hitachi SH5处理器
    0x1c2 Thumb处理器
    0x169 MIPS小尾WCE v2处理器

    3.2.2 特征

    Characteristics域包含镜像文件属性的标志。以下加粗的是常用的属性。当前定义了以下值(由低位往高位):

    位置 描述
    0 它表明此文件不包含基址重定位信息,因此必须被加载到其首选基地址上。如果基地址不可用,加载器会报错。
    1 它表明此镜像文件是合法的。看起来有点多此一举,但又不能少。
    2 保留,必须为0。
    3
    4
    5 应用程序可以处理大于2GB的地址。
    6 保留,必须为0。
    7
    8 机器类型基于32位体系结构。
    9 调试信息已经从此镜像文件中移除。
    10 如果此镜像文件在可移动介质上,完全加载它并把它复制到交换文件中。几乎不用
    11 如果此镜像文件在网络介质上,完全加载它并把它复制到交换文件中。几乎不用
    12 此镜像文件是系统文件,而不是用户程序。
    13 此镜像文件是动态链接库(DLL)。
    14 此文件只能运行于单处理器机器上。
    15 保留,必须为0。

    Windows子系统

    为NT头第2部分的Subsystem域定义了以下值以确定运行镜像所需的Windows子系统(如果存在):

    描述
    0 未知子系统
    1 设备驱动程序和Native Windows进程
    2 Windows图形用户界面(GUI)子系统(一般程序)
    3 Windows字符模式(CUI)子系统(从命令提示符启动的)
    7 Posix字符模式子系统
    9 Windows CE
    10 可扩展固件接口(EFI)应用程序
    11 带引导服务的EFI驱动程序
    12 带运行时服务的EFI驱动程序
    13 EFI ROM镜像
    14 XBOX

    DLL特征

    为NT头的DllCharacteristics域定义了以下值:

    位置 描述
    1 保留,必须为0。
    2
    3
    4
    5 官方文档缺失
    6 官方文档缺失
    7 DLL可以在加载时被重定位。
    8 强制进行代码完整性校验。
    9 镜像兼容于NX。
    10 可以隔离,但并不隔离此镜像。
    11 不使用结构化异常(SE)处理。
    12 不绑定镜像。
    13 保留,必须为0。
    14 WDM驱动程序。
    15 官方文档缺失
    16 可以用于终端服务器。

    每个数据目录给出了Windows使用的表或字符串的地址和大小。这些数据目录项全部被被加载进内存以备系统运行时使用。数据目录是按照如下格式定义的一个8字节结构:

    typedef struct
      DWORD VirtualAddress;  //数据的RVA
      DWORD Size;            //数据的大小
    typedef ENDS

    第1个域——VirtualAddress,实际上是表的RVA。相对镜像基址偏移地址。NT头第2部分的ImageBase
    第2个域给出了表的大小(以字节计)。数据目录组成了NT头的最后一部分。

    Certificate Table域指向属性证书表。它的第一个域是一个文件指针,而不是通常的RVA。

    3.3 节头

    在镜像文件中,每个节的RVA值必须由链接器决定。这样能够保证这些节位置相邻且按升序排列,并且这些RVA值必须是NT头中SectionAlignment域的倍数。

    每个节头(节表项)格式如下,共40个字节:

    偏移 大小 英文名 描述
    0 8 Name 这是一个8字节ASCII编码的字符串,不足8字节时用NULL填充,必须使其达到8字节。如果它正好是8字节,那就没有最后的NULL字符。可执行镜像不支持长度超过8字节的节名。
    8 4 VirtualSize 当加载进内存时这个节的大小。如果此值比SizeOfRawData大,那么多出的部分用0填充。这是节的数据在没有进行对齐处理前的实际大小,不需要内存对齐。
    12 4 VirtualAddress 内存中节相对于镜像基址的偏移。必须是SectionAlignment的整数倍。
    16 4 SizeOfRawData 磁盘文件中初始化数据的大小。它必须是NT头中FileAlignment域的倍数。当节中仅包含未初始化的数据时,这个域应该为0。
    20 4 PointerToRawData 节中数据起始的文件偏移。它必须是NT头中FileAlignment域的倍数。当节中仅包含未初始化的数据时,这个域应该为0。
    24 4 PointerToRelocations 重定位项开头的文件指针。对于可执行文件或没有重定位项的文件来说,此值应该为0。
    28 4 已经废除。
    32 2 NumberOfRelocations 节中重定位项的个数。对于可执行文件或没有重定位项的文件来说,此值应该为0。
    34 2 已经废除。
    36 4 Characteristics 描述节特征的标志。参考“节标志”。

    3.3.1 节标志

    节头中的Characteristics标志指出了节的属性。(以下加粗的是常用的属性值)

    位置 描述
    1 已经废除
    2
    3
    4
    5
    6 此节包含可执行代码。代码段才用“.text”
    7 此节包含已初始化的数据。“.data”
    8 此节包含未初始化的数据。“.bss”
    9 已经废除
    10
    11
    12
    13
    14
    15
    16 此节包含通过全局指针(GP)来引用的数据。
    17 已经废除
    18
    19
    20
    21
    22
    23
    24
    25 此节包含扩展的重定位信息。
    26 此节可以在需要时被丢弃。
    27 此节不能被缓存。
    28 此节不能被交换到页面文件中。
    29 此节可以在内存中共享。
    30 此节可以作为代码执行。
    31 此节可读。(几乎都设置此节)
    32 此节可写。

    第25标志表明节中重定位项的个数超出了节头中为每个节保留的16位所能表示的范围(也就是65535个函数)。如果设置了此标志并且节头中的NumberOfRelocations域的值是0xffff,那么实际的重定位项个数被保存在第一个重定位项的VirtualAddress域(32位)中。如果设置了第25标志但节中的重定位项的个数少于0xffff,则表示出现了错误。

    4 一些注意信息

    1.PE头是怎么计算的?

    SizeOfHeaders所指的头是从文件的第1个字节开始算起的,而不是从PE标记开始算起的。快速的计算方法是从文件的偏移0x3C(第59字节)处获得一个4字节的PE文件签名的偏移地址,这个偏移地址就是本文所定义的DOS头的大小。NT头在32位系统是244字节,在64位系统是260字节。节头的大小由NT头的第1部分的NumberOfSections(节的数量)*40字节(每个节头是40字节)得出。如此,DOS头、NT头、节头3个头的大小加起来并向上舍入为FileAlignment(文件对齐)的正整数倍的最小值就是SizeOfHeaders(头大小)值。

    2.节数量的问题

    Windows读取NumberOfSections的值然后检查节表里的每个结构,如果找到一个全0结构就结束搜索,否则一直处理完NumberOfSections指定数目的结构。没有规定节头必须以全0结构结束。所以加载器使用了双重标准——全0、达到NumberOfSections数量就不再搜索了。

    3.未初始化问题

    ①未初始化数据在文件中是不占空间的,但在内存里还是会占空间的,它们依然依据指定的大小存在内存里。所以说未初始化数据只在文件大小上有优势,在内存里与已初始化数据是一样的。
    ②未初始化数据的方法有2种:1是通过节头的VirtualSize>SizeOfRawData。未初始化数据的大小就是VirtualSize-SizeOfRawData的值。2是节特征的标志置为“此节包含未初始化的数据”,这时SizeOfUninitializedData才会非0。现在 都使用第1种,把它们集成到.data里面可以加快速度。

    4.已初始化问题

    数据目录里面所对应的块中除了属性证书表、调试信息和几个废除的目录项外,全都属于SizeOfInitializedData(已初始化数据大小)范围。当然,已初始化数据不只这些,还可以是常见的代码段等等。

    5.节对齐的问题

    如果NT头的SectionAlignment域的值小于相应操作系统(有些资料说是根据CPU来的,这不一定。因为CPU本身就允许改分页大小,只是大部分时候操作系统是用CPU默认值的。x86平台默认页面大小是4K。IA-64平台默认页面大小是8K。MIPS平台默认页面大小是4K。Itanium平台默认页面大小是8K。)平台的页面大小,那么镜像文件有一些附加的限制。对于这种文件,当镜像被加载到内存中时,节中数据在文件中的位置必须与它在内存中的位置相等,因此节中数据的物理偏移与RVA相同。

    6.镜像大小

    SizeOfImage所代表的内存镜像大小没有包含属性证书表和调试信息,这是因为加载器并不将属性证书和调试信息映射进内存。同时加载器规定,属性证书和调试信息必须被放在镜像文件的最后,并且属性证书表在调试信息节之前。

    7.数据的组织

    CPU的段主要分为4个:代码段、数据段、堆栈段、附加段。而操作系统给程序员留下只有代码段和数据段,堆栈段和附加段就由系统自行处理了,我们不用管。PE文件的数据组织方式是以BaseOfCode、BaseOfData为基准,以节为主体,以数据目录为辅助。
    ①BaseOfCode、BaseOfData是与后面相应的代码节、数据节的VirtualAddress一致。(这里的数据节是狭义的数据节,是特指代码段、数据目录所指定的数据除外的那一部分,也就是我们编程时定义的常量、变量、未初始化数据等)
    ②所有的代码、数据都必须在节里面,否则就算是代码基址、数据基址、数据目录都有指定,而节头里没有指定,加载器也会报错,不能运行
    ③导入函数、导出函数、资源、重定位表等是为了辅助程序主体的,这些都由系统负责处理

    5 特殊的节

    下表描述了保留的节以及它们的属性,后面是对出现在可执行文件中的节的详细描述。这些节是微软的编译产品所定义的不是系统定义的,实际可以不拘泥于此。

    节名 内容
    .bss 未初始化的数据
    .data 代码节
    .edata 导出表
    .idata 导入表
    .idlsym 包含已注册的SEH,它们用以支持IDL属性
    .pdata 异常信息
    .rdata 只读的已初始化数据(用于常量)
    .reloc 重定位信息
    .rsrc 资源目录
    .sbss 与GP相关的未初始化数据
    .sdata 与GP相关的已初始化数据
    .srdata 与GP相关的只读数据
    .text 默认代码节

    5.1 .edata节

    文件A的函数K被文件B调用时,函数K就称为导出函数。导出函数通常出现在DLL中,也可以是exe文件。

    下表描述了导出节的一般结构。

    表名 描述
    导出目录表 它给出了其它各种导出表的位置和大小。
    导出地址表 一个由导出函数的RVA组成的数组。它们是导出的函数和数据在代码节和数据节内的实际地址。其它镜像文件可以通过使用这个表的索引(序数)来调用函数。
    导出名称指针表 一个由指向导出函数名称的指针组成的数组,按升序排列。大小写敏感。
    导出序数表 一个由对应于导出名称指针表中各个成员的序数组成的数组。它们的对应是通过位置来体现的,因此导出名称指针表与导出序数表成员数目必须相同。
    导出名称表 一系列以NULL结尾的ASCII码字符串。导出名称指针表中的成员都指向这个区域。它们都是公用名称,函数导入与导出就是通过它们。

    当其它镜像文件通过名称导入函数时,Win32加载器通过导出名称指针表来搜索匹配的字符串。如果找到,它就查找导出序数表中相应的成员(也就是说,将找到的导出名称指针表的索引作为导出序数表的索引来使用)来获取与导入函数相关联的序数。获取的这个序数是导出地址表的索引,这个索引对应的元素给出了所需函数的实际位置。每个导出函数都可以通过序数进行访问。

    当其它镜像文件通过序数导入函数时,就不再需要通过导出名称指针表来搜索匹配的字符串。因此直接使用序数效率会更高。但是导出名称容易记忆,它不需要用户记住各个符号在表中的索引。

    5.1.1 导出目录表

    导出目录表是导出函数信息的开始部分,它描述了导出函数信息中其余部分的内容。

    偏移 大小 英文名 描述
    0 4 Export Flags 保留,必须为0。
    4 4 Time/Date StampMajor Version 导出函数被创建的日期和时间。这个值与NT头的第一部分TimeDateStamp相同。
    8 2 Major Version 主版本号。
    10 2 Minor Version 次版本号。
    12 4 Name RVA 包含这个DLL全名的ASCII码字符串RVA。以一个NULL字节结尾。
    16 4 Ordinal Base 导出函数的起始序数值。它通常被设置为1。
    20 4 NumberOfFunctions 导出函数中所有元素的数目。
    24 4 NumberOfNames 导出名称指针表中元素的数目。它同时也是导出序数表中元素的数目。
    28 4 AddressOfFunctions 导出地址表RVA。
    32 4 AddressOfNames 导出名称指针表RVA。
    36 4 AddressOfNameOrdinals 导出序数表RVA。

    5.1.2 导出地址表(Export Address Table,EAT)

    导出地址表的格式为下表所述的两种格式之一。如果指定的地址不是位于导出节(其地址和长度由NT头给出)中,那么这个域就是一个Export RVA;否则这个域是一个Forwarder RVA,它给出了一个位于其它DLL中的符号的名称。

    偏移 大小 描述
    0 4 Export RVA 当加载进内存时,导出函数RVA。
    0 4 Forwarder RVA 这是指向导出节中一个以NULL结尾的ASCII码字符串的指针。这个字符串必须位于Export Table(导出表)数据目录项给出的范围之内。这个字符串给出了导出函数所在DLL的名称以及导出函数的名称(例如“MYDLL.expfunc”),或者DLL的名称以及导出函数的序数值(例如“MYDLL.#27”)。

    Forwarder RVA导出了其它镜像中定义的函数,使它看起来好像是当前镜像导出的一样。因此对于当前镜像来说,这个符号同时既是导入函数又是导出函数。

    例如对于Windows XP系统中的Kernel32.dll文件来说,它导出的“HeapAlloc”被转发到“NTDLL.RtlAllocateHeap”。这样就允许应用程序使用Windows XP系统中的Ntdll.dll模块而不需要实际包含任何相关的导入信息。应用程序的导入表只与Kernel32.dll有关。

    导出地址表的的值有时为0,此时表明这里没有导出函数。这是为了能与以前版本兼容,省去修改的麻烦。

    5.1.3 导出名称指针表

    导出名称指针表是由导出名称表中的字符串的地址(RVA)组成的数组。二进制进行排序的,以便于搜索。

    只有当导出名称指针表中包含指向某个导出名称的指针时,这个导出名称才算被定义。换句话说,导出名称指针表的值有可能为0,这是为了能与前面版本兼容。

    5.1.4 导出序数表

    导出序数表是由导出地址表的索引组成的一个数组,每个序数长16位。必须从序数值中减去Ordinal Base域的值得到的才是导出地址表真正的索引。注意,导出地址表真正的索引真正的索引是从0开始的。由此可见,微软弄出Ordinal Base是找麻烦的。导出序数表的值和导出地址表的索引的值都是无符号数。

    导出名称指针表和导出名称序数表是两个并列的数组,将它们分开是为了使它们可以分别按照各自的边界(前者是4个字节,后者是2个字节)对齐。在进行操作时,由导出名称指针这一列给出导出函数的名称,而由导出序数这一列给出这个导出函数对应的序数。导出名称指针表的成员和导出序数表的成员通过同一个索引相关联。

    5.1.5 导出名称表(Export Name Table,ENT)

    导出名称表的结构就是长度可变的一系列以NULL结尾的ASCII码字符串。 导出名称表包含的是导出名称指针表实际指向的字符串。这个表的RVA是由导出名称指针表的第1个值来确定的。这个表中的字符串都是函数名称,其它文件可以通过它们调用函。

    5.1.6 举例

    ①用序数调用
    当可执行文件用序数调用函数时,该序数就是导出函数地址表的真实索引。如果索引是错误的就有可能出现不可预知的错误。最著名的例子就是Windows XP在升级Server 2补丁之后,有很多程序都不能运行就是这个原因。微软用序数这种方法被大多数危险程序(病毒、木马)所引用,同样的微软自己也用这种方法来使用一些隐含的函数。最后受害者还是广大的用户,因为使用序数方法的绝大部分程序是有着不可告人的目的的。

    ②用函数名调用
    当可执行文件用函数名调用时,加载器会通过AddressOfNames以2进制的方法找到第一个相同的函数名。假如找到的是第X个函数名,则在AddressOfNameOrdinals中取出第X个值,该值再减去Ordinal Base则为函数地址的真实索引。

    5.2.idata节

    首先,您得了解什么是导入函数。一个导入函数是被某模块调用的但又不在调用者模块中的函数,因而命名为“import(导入)”。导入函数实际位于一个或者更多的DLL里。调用者模块里只保留一些函数信息,包括函数名及其驻留的DLL名。现在,我们怎样才能找到PE文件中保存的信息呢? 转到 data directory 寻求答案吧。

    文件中导入信息的典型布局如下:


    典型的导入节布局

    5.2.1 导入目录表

    导入目录表是由导入目录项组成的数组,每个导入目录项对应着一个导入的DLL。最后一个导入目录项是空的(全部域的值都为NULL),用来指明目录表的结尾。

    每个导入目录项的格式如下:

    偏移 大小 描述
    0 4 Import Lookup Table RVA 导入查找表的RVA。这个表包含了每一个导入函数的名称或序数。
    4 4 Time/Date Stamp 当镜像与相应的DLL绑定之后,这个域被设置为这个DLL的日期/时间戳。
    8 4 Forwarder Chain 第一个转发项的索引。
    12 4 Name RVA 包含DLL名称的ASCII码字符串RVA。
    16 4 Import Address RVA 导入地址表的RVA。这个表的内容与导入查找表的内容完全一样。

    5.2.2 导入查找表

    导入查找表是由长度为32位(PE32)或64位(PE32+)的数字组成的数组。其中的每一个元素都是位域,其格式如下表所示。在这种格式中,位31(PE32)或位63(PE32+)是最高位。这些项描述了从给定的DLL导入的所有函数。最后一个项被设置为0(NULL),用来指明表的结尾。

    偏移 大小 位域 描述
    31/63 1 Ordinal/Name Flag 如果这个位为1,说明是通过序数导入的。否则是通过名称导入的。测试这个位的掩码为0x80000000(PE32)或)0x8000000000000000(PE32+)。
    15-0 16 Ordinal Number 序数值(16位长)。只有当Ordinal/Name Flag域为1(即通过序数导入)时才使用这个域。位30-15(PE32)或62-15(PE32+)必须为0。
    30-0 31 Hint/Name Table RVA 提示/名称表项的RVA(31位长)。只有当Ordinal/Name Flag域为0(即通过名称导入)时才使用这个域。对于PE32+来说,位62-31必须为0。

    5.2.3 提示/名称表

    提示/名称表中的每一个元素结构如下:

    偏移 大小 描述
    0 2 Hint 指出名称指针表的索引。当搜索匹配字符串时首选使用这个值。如果匹配失败,再在DLL的导出名称指针表中进行2进制搜索。
    2 可变 Name 包含导入函数名称的ASCII码字符串。这个字符串必须与DLL导出的函数名称匹配。同时这个字符串区分大小写并且以NULL结尾。
    * 0或1 Pad 为了让提示/名称表的下一个元素出现在偶数地址,这里可能需要填充0个或1个NULL字节。

    5.2.4 导入地址表

    导入地址表的结构和内容与导入查找表完全一样,直到文件被绑定。在绑定过程中,用导入函数的32位(PE32)或64位(PE32+)地址覆盖导入地址表中的相应项。这些地址是导入函数的实际内存地址,尽管技术上仍把它们称为“虚拟地址”。加载器通常会处理绑定。

    5.3 .pdata节(可有可无,谁也不希望自己的函数出问题的吧!)

    .pdata节是由用于异常处理的函数表项组成的数组。NT头中的Exception Table(异常表)域指向它。在将它们放进最终的镜像文件之前,这些项必须按函数地址(下列每个结构的第一个域)排序。下面描述了函数表项的3种格式,使用哪一种取决于目标平台。

    对于32位的MIPS镜像来说,其函数表项格式如下:

    偏移 大小 描述
    0 4 Begin Address 相应函数的VA
    4 4 End Address 函数结尾的VA
    8 4 Exception Handler 指向要执行的异常处理程序的指针
    12 4 Handler Data 指向要传递给异常处理程序的附加数据的指针
    16 4 Prolog End Address 函数prolog代码结尾的VA

    对于ARM、PowerPC、SH3和SH4 Windows CE平台来说,其函数表项格式如下:

    偏移 大小 描述
    0 4 Begin Address 相应函数的VA
    4 8位 Prolog Length 函数prolog代码包含的指令数
    4 22位 Function Length 函数代码包含的指令数
    4 1位 32-bit Flag 如果此位为1,表明函数由32位指令组成。否则,函数由16位指令组成。
    4 1位 Exception Flag 如果此位为1,表明存在用于此函数的异常处理程序;否则,不存在异常处理程序。

    对于x64和Itanium平台来说,其函数表项格式如下:

    偏移 大小 描述
    0 4 Begin Address 相应函数的RVA
    4 4 End Address 函数结尾的RVA
    8 4 Unwind Information 用于异常处理的展开(Unwind)信息的RVA

    5.4 .reloc节

    基址重定位表包含了镜像中所有需要重定位的内容。NT头中的数据目录中的Base Relocation Table(基址重定位表)域给出了基址重定位表所占的字节数。基址重定位表被划分成许多块,每一块表示一个4K页面范围内的基址重定位信息,它必须从32位边界开始。

    5.4.1 基址重定位块

    每个基址重定位块的开头都是如下结构:

    偏移 大小 描述
    0 4 Page RVA 将镜像基址与这个域(页面RVA)的和加到每个偏移地址处最终形成一个VA,这个VA就是要进行基址重定位的地方。
    4 4 Block Size 基址重定位块所占的总字节数,其中包括Page RVA域和Block Size域以及跟在它们后面的Type/Offset域。

    Block Size域后面跟着数目不定的Type/Offset位域。它们中的每一个都是一个WORD(2字节),其结构如下:

    偏移 大小 描述
    0 4位 Type 它占这个WORD的最高4位,这个值指出需要应用的基址重定位类型。参考5.4.2节“基址重定位类型”。
    0 12位 Offset 它占这个WORD的其余12位,这个值是从基址重定位块的Page RVA域指定的地址处开始的偏移。这个偏移指出需要进行基址重定位的位置。

    为了进行基址重定位,需要计算镜像的首选基地址与实际被加载到的基地址之差。如果镜像本身就被加载到了其首选基地址,那么这个差为零,因此也就不需要进行基址重定位了。

    5.4.2 基址重定位类型

    描述
    0 基址重定位被忽略。这种类型可以用来对其它块进行填充。
    1 基址重定位时将差值的高16位加到指定偏移处的一个16位域上。这个16位域是一个32位字的高半部分。
    2 基址重定位时将差值的低16位加到指定偏移处的一个16位域上。这个16位域是一个32位字的低半部分。
    3 基址重定位时将所有的32位差值加到指定偏移处的一个32位域上。
    4 进行基址重定位时将差值的高16位加到指定偏移处的一个16位域上。这个16位域是一个32位字的高半部分,而这个32位字的低半部分被存储在紧跟在这个Type/Offset位域后面的一个16位字中。也就是说,这一个基址重定位项占了两个Type/Offset位域的位置。
    5 对MIPS平台的跳转指令进行基址重定位。
    6 保留,必须为0
    7 保留,必须为0
    9 对MIPS16平台的跳转指令进行基址重定位。
    10 进行基址重定位时将差值加到指定偏移处的一。

    5.5 加载配置结构(不清楚,大概又是多余的吧)

    加载配置结构最初用于Windows NT操作系统自身几种非常有限的场合——在镜像文件头或NT头中描述各种特性太困难或这些信息尺寸太大。当前版本的Microsoft链接器和Windows XP以及后续版本的Windows使用的是这个结构的新版本,将之用于包含保留的SEH技术的基于x86的32位系统上。它提供了一个安全的结构化异常处理程序列表,操作系统在进行异常派送时要用到这些异常处理程序。如果异常处理程序的地址在镜像的VA范围之内,并且镜像被标记为支持保留的SEH,那么这个异常处理程序必须在镜像的已知安全异常处理程序列表中,否则操作系统将终止这个应用程序。这是为了防止利用“x86异常处理程序劫持”来控制操作系统,它在以前已经被利用过。

    Microsoft的链接器自动提供一个默认的加载配置结构来包含保留的SEH数据。如果用户的代码已经提供了一个加载配置结构,那么它必须包含新添加的保留的SEH域。否则,链接器将不能包含保留的SEH数据,这样镜像文件就不能被标记为包含保留的SEH。

    5.5.1 加载配置目录

    对应于预保留的SEH加载配置结构的数据目录项必须为加载配置结构指定一个特别的大小,因为操作系统加载器总是希望它为这样一个特定值。事实上,这个大小只是用于检查这个结构的版本。为了与Windows XP以及以前版本的Windows兼容,x86镜像文件中这个结构的大小必须为64。

    5.5.2 加载配置结构布局

    用于32位和64位PE文件的加载配置结构布局如下:

    偏移 大小 描述
    0 4 Characteristics 指示文件属性的标志,当前未用。
    4 4 TimeDateStamp 日期/时间戳。这个值表示从UTC时间1970年1月1日午夜(00:00:00)以来经过的总秒数,它是根据系统时钟算出的。可以用C运行时函数time来获取这个时间戳。
    8 2 MajorVersion 主版本号
    10 2 MinorVersion 次版本号
    12 4 GlobalFlagsClear 当加载器启动进程时,需要被清除的全局加载器标志。
    16 4 GlobalFlagsSet 当加载器启动进程时,需要被设置的全局加载器标志。
    20 4 CriticalSectionDefaultTimeout 用于这个进程处于无约束状态的临界区的默认超时值。
    24 8 DeCommitFreeBlockThreshold 返回到系统之前必须释放的内存数量(以字节计)。
    32 8 DeCommitTotalFreeThreshold 空闲内存总量(以字节计)。
    40 8 LockPrefixTable [仅适用于x86平台]这是一个地址列表的VA。这个地址列表中保存的是使用LOCK前缀的指令的地址,这样便于在单处理器机器上将这些LOCK前缀替换为NOP指令。
    48 8 MaximumAllocationSize 最大的分配粒度(以字节计)。
    56 8 VirtualMemoryThreshold 最大的虚拟内存大小(以字节计)。
    64 8 ProcessAffinityMask 将这个域设置为非零值等效于在进程启动时将这个设定的值作为参数去调用SetProcessAffinityMask函数(仅适用于.exe文件)。
    72 4 ProcessHeapFlags 进程堆的标志,相当于函数的第一个参数。这些标志用于在进程启动过程中创建的堆。
    76 2 CSDVersion Service Pack版本标识。
    78 2 Reserved 必须为0
    80 8 EditList 保留,供系统使用。
    60/88 4/8 SecurityCookie 指向cookie的指针。cookie由Visual C++编译器的GS实现所使用。
    64/96 4/8 SEHandlerTable [仅适用于x86平台]这是一个地址列表的VA。这个地址列表中保存的是镜像中每个合法的、独一无二的SE处理程序的RVA,并且它们已经按RVA排序。
    68/104 4/8 SEHandlerCount [仅适用于x86平台]表中独一无二的SE处理程序的数目。

    5.6 .rsrc节

    资源节可以看成是一个磁盘的分区,盘符是资源目录表,下面有3层目录(资源目录项),最后是文件(资源数据)。

    ①资源目录表是一个16字节组成的结构。其第一个字节又称为“根节点”。其前的12字节虽然有定义,但加载器并不理会,所以任何值都可以。

    ②第1层目录(资源目录项)是资源类型,微软已经定义了21种。其结构是一个16字节的数组。资源目录项分为名称项和ID项,这取决于资源目录表。资源目录表指出跟着它的名称项和ID项各有多少个(表中所有的名称项在所有的ID项前面)。表中的所有项按升序排列:名称项是按不区分大小写的字符串,而ID项则是按数值。第0-3字节表示资源类型的名称字符串的地址或是32位整数,第4-7字节表示第二层目录(资源目录项)相对于根节点的偏移。

    一系列资源目录表按如下方式与各层相联系:每个目录表后面跟着一系列目录项,它们给出那个层(类型、名称或语言)的名称或标识(ID)及其数据描述或另一个目录表的地址。如果这个地址指向一个数据描述,那么那个数据就是这棵树的叶子。如果这个地址指向另一个目录表,那么那个目录表列出了下一层的目录项。

    一个叶子的类型、名称和语言ID由从目录表到这个叶子的路径决定。第1个表决定类型ID,第2个表(由第一个表中的目录项指向)决定名称ID,第3个表决定语言ID。

    .rsrc节的一般结构如下:

    数据 描述
    资源目录表 所有的顶层(类型)结点都被列于第1个表中。这个表中的项指向第2层表。每个第2层树的类型ID相同但是名称ID不同。第3层树的类型ID和名称ID都相同但语言ID不同。每个单个的表后面紧跟着目录项,每一项都有一个名称或数字标识和一个指向数据描述或下一层表的指针。
    资源目录项  
    资源目录字符串 按2字节边界对齐的Unicode字符串,它是作为由资源目录项指向的字符串数据来使用的。
    资源数据描述 一个由记录组成的数组,由表指向它,描述了资源数据的实际大小和位置。这些记录是资源描述树中的叶子。
    资源数据 资源节的原始数据。资源据描述域中的大小和位置信息将资源数据分成单个的区域。

    资源目录表

    偏移 大小 描述
    0 4 Characteristics 资源标志。保留供将来使用。当前它被设置为0。
    4 4 Time/Date Stamp 资源数据被资源编译器创建的时间。
    8 2 Major Version 主版本号,由用户设定。
    10 2 Minor Version 次版本号,由用户设定。
    12 2 Number of Name Entries 紧跟着这个表头的目录项的个数,这些目录项使用名称字符串来标识类型、名称或语言项。
    14 2 Number of ID Entries 紧跟着这个表头的目录项的个数,这些目录项使用数字来标识类型、名称或语言项。

    资源目录项

    具体的情况是资源目录表后面紧跟着以名称项和ID项所组成的数组。资源目录表与资源目录项之间不能有空隙。名称项组成的数组在ID项组成的数组前面,且两个数组不能有空隙。

    偏移 大小 描述
    0 4 Name RVA 表示类型、名称或语言ID项的名称字符串的地址。
    0 4 Integer ID 表示类型、名称或语言ID项的32位整数。
    4 4 Data Entry RVA 最高位为0。低31位是资源数据项的地址。
    4 4 Subdirectory RVA 最高位为1。低31位是另一个资源目录表(下一层)的地址。

    资源目录字符串

    资源目录字符串区由按字边界对齐的Unicode字符串组成。这些字符串被存储在最后一个资源目录项之后、第一个资源数据项之前。这样能够使这些长度可变的字符串对长度固定的目录项的对齐情况影响最小。每个资源目录字符串格式如下:

    偏移 大小 描述
    0 2 Length 字符串的长度,不包括Length域本身。
    2 可变 Unicode String 可变 Unicode String 按字边界对齐的可变长度的Unicode字符串。

    资源数据项

    每个资源数据项描述了资源数据区中一个实际单元的原始数据。资源数据项格式如下:

    偏移 大小 描述
    0 4 Data RVA 资源数据区中一个单元的资源数据的地址。
    4 4 Size 由Data RVA域指向的资源数据的大小(以字节计)。
    8 4 Codepage 用于解码资源数据中的代码点值的代码页。通常这个代码页应该是Unicode代码页。
    12 4 保留,必须为0 保留,必须为0

    6 属性证书表

    可以给镜像文件添加属性证书表使它与属性证书相关联。有多种不同类型的属性证书,最常用的是Authenticode签名。

    属性证书表包含一个或多个长度固定的表项,可以通过NT头中的数据目录中的Certificate Table(证书表)域找到它们。这个表的每个表项给出了相应证书的开始位置和长度。存储在这个节中的每个证书都有一个相应的证书表项。证书表项的数目可以通过将证书表的大小除以证书表中每一项的大小(8)得到。注意证书表的大小仅包括它的表项,并不包括这些表项实际指向的证书。

    每个表项格式如下:

    偏移 大小 描述
    0 4 Certificate Data 指向证书实际数据的文件指针。它指向的地址总是按8字节倍数边界(即最低3个位都是0)对齐。
    0 4 Size of Certificate 这是一个无符号整数,它指出证书的大小(以字节计)。

    注意证书总是从8进制字(从任意字节边界开始的16个连续字节)边界开始。如果一个证书的长度不是8进制字长度的偶数倍,那么就一直用0填充到下一个八进制字边界。但是证书长度并不包括这些填充的0。因此任何处理证书的软件必须向上舍入到下一个8进制字才能找到另一个证书。

    证书的起始位置和长度由证书表中相应的表项给出。每个证书都有惟一一个与它对应的表项。




    展开全文
  • 众所周知,每种基本数据类型都有一个固定的位数,比如byte占8,short占16,int占32等。正因如此,当把一个低精度的数据类型转成一个高精度的数据类型时,必然会涉及到如何扩展位数的问题。这里有两种解决方案...

    众所周知,每种基本数据类型都有一个固定的位数,比如byte占8位,short占16位,int占32位等。正因如此,当把一个低精度的数据类型转成一个高精度的数据类型时,必然会涉及到如何扩展位数的问题。这里有两种解决方案:
    (1)补零扩展:填充一定位数的0。
    (2)补符号位扩展:填充一定位数的符号位(非负数填充0,负数填充1)。
    对于无符号类型(相当于都是非负数)与有符号类型中的非负数部分,这两种方法没有区别,都是填充0;对于有符号类型中的负数部分,这两种方法就会产生差异了,补零扩展会填充0,而补符号位扩展会填充1。下面将byte类型的-127转为int类型为例,探讨一下这两种方法的区别。

     

    首先必须明确一些知识点:

    • 计算机是用补码来存储数字的;
    • 一个数的补码的补码等于原码。

    反码、补码

    首先,我们从位权的含义说起。例如,十进制39的各个数位的数值,并不是简单的3和9,这点大家都知道,3表示的是3x10,9表示的是9x1。这里和各个数位的数值相乘的10和1,就是位权。数字的位数不同,位权也不同。第一位(最右边的一位)是10的0次幂,第二位是10的1次幂....以此类推。

    位权的思考方式同样适用于二进制。即第一位是2的0次幂,第二位是2的1次幂.... “OO的XX次幂”表示位权,其中,十进制数的情况下OO部分为10,二进制的情况为2,这个则称为基数

    在日常生活当中,可以看到很多这样的事情:

    1. 把某物体左转 90 度,和右转 270 度,在不考虑圈数的条件下,最终的效果是相同的;
    2. 把分针倒拨 20 分钟,和正拨 40 分钟,在不考虑时针的条件下,效果也是相同的;
    3. 把数字 87,减去 25,和加上 75,在不考虑百位数的条件下,效果也是相同的;
    4. ……。

    上述几组数字,有这样的关系:

    •   90 + 270 = 360
    •   20 + 40 = 60
    •   25 + 75 = 100

    式中的 360、60 和 100,就是“”。
    式中的 90 和 270、20 和 40,以及 25 和 75,就是一对对“互补”的数字。

    知道了“模”,求某个数字的“补数”,就是轻而易举的了:
    如果模为 365,数字 120 的补数为:365 - 120 = 245。

    用补数代替原数,可把减法转变为加法。出现的进位就是模,此时的进位,就应该忽略不计。

    接下来我们正式引入补数:

    二进制数中表示负数值时,一般把最高位作为符号位来使用,符号位为0时表示正数,为1时表示负数。

    那么-1用八位二进制来表示的话是怎么样的呢?可能很多人认为1的二进制'0000 0001'(常规思维),因此-1的二进制就是‘1000 0001’。但这个答案是错位的,正确答案是‘1111 1111’(计算机补码形式)。(PS:有些教程可能写0000 0001,它可能非计算机八位二进制补码形式,而查用我们数学表达?而下面使用符号位不变取反也是这批人。)

    而计算机里面,只有加法器,没有减法器,所有的减法运算,都必须用加法进行。计算机再做减法运算时,实际上内部是在做加法运算,在表示负数时就需要使用“二进制的补数”补数就是用正数来表示负数,很不可思议吧。

    为了获得补数,我们需要将二进制的各数位的数值全部取反,然后再将结果加一。例如,用八位二进制表示-1时,只需求得1,也就是0000 0001得补数即可。具体来说,就是将各位数得0取反得到1,1取反成0,然后将取反得结果为1,最后就转化为了1111 1111。

    (ps:国内还有一种算法就是符号位不变,balala.. 按照上面钟表的例子,其实也行的通,但是其似乎偏离了设计者得本意与它得本质)

    补码的思考方式,虽然直观上不易理解,但逻辑上非常严谨,例如1-1也就是1+(-1)这一运算,我们都知道答案为0。首先,让我们将-1表示为1000 0001(错误方式,原码)来运算,看看结果如何,0000 0001 + 1000 0001 = 1000 0010,很显然结果不是0。

    接下来,我们把-1表示为1111 1111 (补码)来进行运算。 0000 0001 + 1111 1111 = 1 0000 0000 。最高位溢出,对于溢出位,计算机回自动忽略掉。在八位这个范围内计算,1 0000 0000 这个 九位二进制会被认为是 0000 0000 这一八位二进制数。

    请牢记“将二进制数的值取反后加一的结果,和原来的值相加,结果为零”这一法则。

    那么 1111 1110 表示的负数是多少大家知道吗?这时,我们可以利用负负得正的性质,假若1111 1110是负🔺🔺,那么1111 1110的补数就是正🔺🔺。通过求解补数的补数,就可知道该值的绝对值。1111 1110的补数,取反加1后为0000 0010。这个是2的十进制数,因此1111 1110表示的就是-2。

    另外,关于下面网上说法,我不知道其观点的具体含义,我在中文维基百科看到有这样描述,但在英文版似乎没有发现。 这里也不是理解正数的补码与负数的补码这种说法。

    • 正数的补码等于原码; 
    • 负数的补码等于反码+1;

    我认为,补码不过是表示负数的一种方式,补码不是相对正数的吗?"正数的补码"有何含义吗?希望有人给我解答其具体含义?

    高级程序设计语言允许程序员使用包含不同字节大小整数的对象表达式。

    那么,当一个表达式的两个操作数大小不同时,有些语言会报错,有些语言则会自动将操作数转换成一个统一的格式。这种转换是有代价的,因此如果你不希望编译器在你不知情的情况下自动加入各种转换到原本非常完美的代码中,你就需要掌握编译器如何处理这些表达式。

    零扩展

    在移动或转换操作中,零扩展是指将目标的高位设置为零,而不是将其设置为源的最高有效位的副本。 如果操作的源是无符号数字,则零扩展通常是在保留其数值的同时将其移至更大字段的正确方法,而符号扩展对于有符号数字是正确的。

    高位直接补0的扩展,如1111变成00001111,补0并不影响计算结果,这个很好理解,但如果二进制数带了符号,就不一样了,因为最高位是符号位,所以1111就从一个负数,变成了一个正数00001111,由此,产生了符号扩展。

    在x86和x64指令集中,movzx指令(“零扩展移动”)执行此功能。 例如,movzx ebx,al将一个字节从al寄存器复制到ebx的低位字节,然后用零填充ebx的其余字节。

    在x64上,大多数写入任何通用寄存器的低32位的指令都会将目标寄存器的高一半置零。 例如,指令mov eax,1234将清除rax寄存器的高32位。

    符号扩展

    符号扩展是计算机算术中在保留数字的符号(正/负)和值的同时增加二进制数的位数的操作。 这是通过根据所使用的特定带符号的数字表示的过程,将数字附加到数字的最高有效位来完成的。

    例如,如果使用六位表示数字“ 00 1010”(十进制正数10),并且符号扩展操作将字长增加到16位,则新的表示形式就是“ 0000 0000 0000 1010”。因此,既保持了价值,又保持了价值为正的事实。

    如果用10位表示用二进制补码值“1111110001”(十进制负15),并且将其符号扩展为16位,则新表示为“1111 1111 1111 0001 ”。因此,通过在左侧填充ones,可以保持负号和原始编号的值。 1111 1111 1111 0001

    例如,在Intel x86指令集中,有两种方式进行符号扩展:

    • 使用指令cbw,cwd,cwde和cdq:分别将字节转换为字,将字转换为双字,将字转换为扩展双字和将双字转换为四字(在x86上下文中,一个字节有8位,一个字有16位,一个双字和扩展的双字32位和四字64位);
    • 使用由movsx(“带符号扩展的移动”)指令系列完成的符号扩展移动之一。

    实例

    举个例子:

    -127原码1111 1111,反码1000 0000,补码1000 0001。计算机存储的是1000 0001,用十六进制表示为0x81。

    • 当使用补零扩展时,结果为:

    0000 0000 0000 0000 0000 0000 1000 0001 (与补码数值形式一致)
    用十六进制表示为0x81。为了计算十进制值,计算它的补码,结果为:
    0000 0000 0000 0000 0000 0000 1000 0001
    将这个二进制数转成十进制的结果是129。

    • 当使用补符号位扩展时,结果为:

    1111 1111 1111 1111 1111 1111 1000 0001 (和补码数值看上去差别较大)
    用十六进制表示为0xFFFFFF81。为了计算十进制值,计算它的补码,结果为:
    1000 0000 0000 0000 0000 0000 0111 1111
    将这个二进制数转成十进制的结果是-127。

    由此可以得出结论:
    (1)使用补零扩展能够保证二进制存储的一致性(和我们数学常理一致),但不能保证十进制值不变。所以,处理无符号二进制数的时候,可以使用零扩展(zero extension)将小位数的无符号数扩展到大位数的无符号数
    (2)使用补符号位扩展能够保证十进制值不变,但不能保证二进制存储的一致性(负数的补码变了,需要 &0xff)而处理不同长度的有符号数时,我们必须使用符号扩展

    在C/C++中,如果把一个char向一个整形转换的时候,就会存在着这个问题。

    如果你想得到一个正数,那么如果一个字符的ASCII码值是小于零的,而直接用(int)c进行强制类型转换,结果是通过符号扩展得到的也为一个负数。

    要得到正数,一定要用(int)(unsigned char)c;因为unsigned char去除了c的符号位,所以,这样的类型转换后,再用(int)进行转换得到的就是一个正数。

    #include <iostream> 
    #include <string>
    #include <algorithm>
    #include <bitset>       
    
    int main()
    {
    
    	int i = 129;
    	char chA = (char)i;
    	int c = (int)(unsigned char)chA;
    	int b = (int)chA;
    
    	std::cout << "sign extension: " << b << std::endl;
    	std::cout << "zero extension: " << c << std::endl;
    
        char d = -127;
    	std::bitset <sizeof(int) * 8> x(d);   
    	std::cout << "sign extension: " << x << std::endl;
    
    	unsigned char e = (d & 0XFF);
    	std::bitset <sizeof(int) * 8> y(e);
    	std::cout << "sign extension: " << y << std::endl;
    	
    	return 0;
    }

     结果

    std::bitset

    std::bitset 是 一种 位集存储位(元素只有两个可能的值:0或1 truefalse,...)。

    • bitset存储二进制数位。
    • bitset就像一个bool类型的数组一样,但是有空间优化——bitset中的一个元素一般只占1 bit(在大多数系统上,相当于一个char元素所占空间的八分之一,一个char占用一个字节byte,8位bits)
    • bitset中的每个元素都能单独被访问,例如对于一个叫做foo的bitset,表达式foo[3]访问了它的第4个元素,就像访问了数组其元素一样。但是,因为在大多数C ++环境中没有元素类型是单个位,所以可以将单个元素作为特殊引用类型进行访问(请参见bitset :: reference)。
    • bitset具有可以从整数值和二进制字符串构造并转换为整数值的功能(请参阅其构造函数和成员to_ulong和to_string)。它们也可以直接以二进制格式插入和从流中提取(请参阅适用的运算符)。
    • bitset的大小在编译时就需要确定(由其模板参数确定)。有关还可优化空间分配并允许动态调整大小的类,请参见vector的布尔特殊化(vector <bool>)。

    同一长度数据类型中,有符号数与无符号数相互转换

    直接将内存中的数据赋给要转化的类型,数值大小则会发生变化。另外,短类型扩展为长类型时,短类型与长类型分属有符号数与无符号数时,则先按规则一进行类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对方。以下是有符号数与无符号数之间的转换:

    有符号数的转换
     

    无符号数的转换 

    参考:

    关于补零扩展与补符号位扩展_swt369的博客-CSDN博客_无符号short型存储后面补零

    C++ 中注意,零扩展和符号位扩展_jaylong35的专栏-CSDN博客_符号位扩展

    符号扩展,零扩展与符号缩减

    《程序是怎样跑起来的》 - 矢泽久雄 

    展开全文
  • 一、前言 Objective-C 使用引用计数作为 iPhone 应用的内存...虽然 Objective-C 通过引入弱引用技术,让开发者可以尽可能地规避这个问题,但在引用层级过深,引用路径不那么直观的情况下,即使是经验丰富的工程师,也

    一、前言

    • Objective-C 使用引用计数作为 iPhone 应用的内存管理方案,引用计数相比 GC 更适用于内存不太充裕的场景,只需要收集与对象关联的局部信息来决定是否回收对象,而 GC 为了明确可达性,需要全局的对象信息。引用计数固然有其优越性,但也正是因为缺乏对全局对象信息的把控,导致 Objective-C 无法自动销毁陷入循环引用的对象。虽然 Objective-C 通过引入弱引用技术,让开发者可以尽可能地规避这个问题,但在引用层级过深,引用路径不那么直观的情况下,即使是经验丰富的工程师,也无法百分百保证产出的代码不存在循环引用。
    • 这时候就需要有一种检测方案,可以实时检测对象之间是否发生了循环引用,来辅助开发者及时地修正代码中存在的内存泄漏问题。要想检测出循环引用,最直观的方式是递归地获取对象强引用的其他对象,并判断检测对象是否被其路径上的对象强引用了,也就是在有向图中去找环。明确检测方式之后,接下来需要解决的是如何获取强引用链,也就是获取对象的强引用,尤其是最容易造成循环引用的 block。

    二、Block 捕获实体引用

    ① 捕获区域布局

    • 根据 block 的定义结构,可以简单地将其视为:
    struct sr_block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct sr_block_descriptor *descriptor;
        /* Imported variables. */
    };
    
    // 标志位不一样,这个结构的实际布局也会有差别,这里简单地放在一起好阅读
    struct sr_block_descriptor {
        unsigned long reserved; 		 // Block_descriptor_1
        unsigned long size; 			 // Block_descriptor_1
        void (*)(void *dst, void *src);  // Block_descriptor_2 BLOCK_HAS_COPY_DISPOSE
        void (*dispose)(void *); 		 // Block_descriptor_2
        const char *signature; 		     // Block_descriptor_3 BLOCK_HAS_SIGNATURE
        const char *layout; 			 // Block_descriptor_3 contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
    
    • 可以看到 block 捕获的变量都会存储在 sr_block_layout 结构体 descriptor 字段之后的内存空间中,通过 clang -rewrite-objc 重写如下代码语句:
    int i = 2;
    ^{
        i;
    };
    
    • 可以得到 :
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int i;
      ...
    };
    
    • __main_block_impl_0 结构中新增了捕获的 i 字段,即 sr_block_layout 结构体的 imported variables 部分,这种操作可以看作在 sr_block_layout 尾部定义了一个 0 长数组,可以根据实际捕获变量的大小,给捕获区域申请对应的内存空间,只不过这一操作由编译器完成:
    struct sr_block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct sr_block_descriptor *descriptor;
        char captured[0];
    };
    
    • 既然已经知道捕获变量 i 的存放地址,那么就可以通过 *(int *)layout->captured 在运行时获取 i 的值,得到捕获区域的起始地址之后,再来看捕获区域的布局问题,考虑以下代码块:
    int i = 2;
    NSObject *o = [NSObject new];
    void (^blk)(void) = ^{
        i;
        o;
    };
    
    • 捕获区域的布局分两部分看:顺序和大小,先使用老方法重写代码块:
    struct __main_block_impl_0 {
      struct __block_impl impl;           // 24
      struct __main_block_desc_0* Desc;   // 8 指针占用内存大小和寻址长度相关,在 64 位机环境下,编译器分配空间大小为 8 字节
      int i;                              // 8
      NSObject *o;                        // 8
      ...
    };
    
    • 按照目前 clang 针对 64 位机的默认对齐方式(下文的字节对齐计算都基于此前提条件),可以计算出这个结构体占用的内存空间大小为 24 + 8 + 8 + 8 = 48字节,并且按照上方代码块先 i 后 o 的捕获排序方式,如果要访问捕获的 o 对象指针变量,只需要在捕获区域起始地址上偏移 8 字节即可,可以借助 lldb 的 memory read (x) 命令查看这部分内存空间:
    (lldb) po *(NSObject **)(layout->captured + 8)
    0x0000000000000002
    (lldb) po *(NSObject **)layout->captured
    <NSObject: 0x10073f290>
    (lldb) p *(int *)(layout->captured + 8)
    (int) $6 = 2
    (lldb) p (int *)(layout->captured + 8)
    (int *) $9 = 0x0000000100740d18
    (lldb) p layout->descriptor->size
    (unsigned long) $11 = 44
    (lldb) x/44bx layout
    0x100740cf0: 0x70 0x21 0x7b 0xa6 0xff 0x7f 0x00 0x00
    0x100740cf8: 0x02 0x00 0x00 0xc3 0x00 0x00 0x00 0x00
    0x100740d00: 0x40 0x1d 0x00 0x00 0x01 0x00 0x00 0x00
    0x100740d08: 0xb0 0x20 0x00 0x00 0x01 0x00 0x00 0x00
    0x100740d10: 0x90 0xf2 0x73 0x00 0x01 0x00 0x00 0x00
    0x100740d18: 0x02 0x00 0x00 0x00
    
    • 和使用 clang -rewrite-objc 重写时的猜想不一样,可以从以上终端日志中看出以下两点:
      • 捕获变量 i、o 在捕获区域的排序方式为 o、i,o 变量地址与捕获起始地址一致,i 变量地址为捕获起始地址加上 8 字节;
      • 捕获整形变量 i 在内存中实际占用空间大小为 4 字节;
    • 那么 block 到底是怎么对捕获变量进行排序,并且为其分配内存空间的呢?这就需要看 clang 是如何处理 block 捕获的外部变量。

    ② 捕获区域布局分析

    • 首先解决捕获变量排序的问题,根据 clang 针对这部分的排序代码,可以知道,在对齐字节数 (alignment) 不相等时,捕获的实体按照 alignment 降序排序 (C 结构体比较特殊,即使整体占用空间比指针变量大,也排在对象指针后面),否则按照以下类型进行排序:
      • __strong 修饰对象指针变量;
      • __block 修饰对象指针变量;
      • __weak 修饰对象指针变量;
      • 其他变量;
    • 再结合 clang 对捕获变量对齐子节数计算方式 ,可以知道,block 捕获区域变量的对齐结果趋向于被 attribute ((packed)) 修饰的结构体,举个例子:
    struct foo {
        void *p;    // 8
        int i;      // 4
        char c;     // 4 实际用到的内存大小为 1
    };
    
    • 创建 foo 结构体需要分配的空间大小为 8 + 4 + 4 = 16,关于结构体的内存对齐方式,编译器会按照成员列表的顺序一个接一个地给每个成员分配内存,只有当存储成员需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间,以提升计算机的访问速度(对齐标准一般和寻址长度一致),在声明结构体时,让那些对齐边界要求最严格的成员最先出现,对边界要求最弱的成员最后出现,可以最大限度地减少因边界对齐而带来的空间损失。再看以下代码块:
    struct foo {
        void *p;    // 8
        int i;      // 4
        char c;     // 1
    } __attribute__ ((__packed__));
    
    • attribute ((packed)) 编译属性会告诉编译器,按照字段的实际占用子节数进行对齐,所以创建 foo 结构体需要分配的空间大小为 8 + 4 + 1 = 13。
    • 结合以上两点,可以尝试分析以下 block 捕获区域的变量布局情况:
    NSObject *o1 = [NSObject new];
    __weak NSObject *o2 = o1;
    __block NSObject *o3 = o1;
    unsigned long long j = 4;
    int i = 3;
    char c = 'a';
    void (^blk)(void) = ^{
        i;
        c;
        o1;
        o2;
        o3;
        j;
    };
    
    • 按照 aligment 排序,可以得到排序顺序为 [o1 o2 o3] j i c,再根据 __strong、__block、__weak 修饰符对 o1 o2 o3 进行排序,可得到最终结果 o1[8] o3[8] o2[8] j[8] i[4] c[1]。同样的,我们使用 lldb 的 x 命令验证分析结果是否正确:
    (lldb) x/69bx layout
    0x10200d940: 0x70 0x21 0x7b 0xa6 0xff 0x7f 0x00 0x00
    0x10200d948: 0x02 0x00 0x00 0xc3 0x00 0x00 0x00 0x00
    0x10200d950: 0xf0 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x10200d958: 0xf8 0x20 0x00 0x00 0x01 0x00 0x00 0x00
    0x10200d960: 0xa0 0xf6 0x00 0x02 0x01 0x00 0x00 0x00  // o1
    0x10200d968: 0x90 0xd9 0x00 0x02 0x01 0x00 0x00 0x00  // o3
    0x10200d970: 0xa0 0xf6 0x00 0x02 0x01 0x00 0x00 0x00  // o2
    0x10200d978: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00  // j
    0x10200d980: 0x03 0x00 0x00 0x00 0x61                 // i c
    (lldb) p o1
    (NSObject *) $1 = 0x000000010200f6a0
    
    • 可以看到,小端模式下,捕获的 o1 和 o2 指针变量值为 0x10200f6a0,对应内存地址为 0x10200d960 和 0x10200d970,而 o3 因为被 __block 修饰,编译器为 o3 捕获变量包装了一层 byref 结构,所以其值为 byref 结构的地址 0x102000d990,而不是 0x10200f6a0,捕获的 j 变量地址为 0x10200d978,i 变量地址为 0x10200d980,c 字符变量紧随其后。

    ③ Descriptor 的 Layout 信息

    • 经过上述的一系列分析,捕获区域变量的布局方式已经大致清楚,接下来回过头看下 sr_block_descriptor 结构的 layout 字段是用来干什么的?从字面上理解,这个字段很可能保存了 block 某一部分的内存布局信息,比如捕获区域的布局信息,依然使用上文的最后一个例子,看看 layout 的值:
    (lldb) p layout->descriptor->layout
    (const char *) $2 = 0x0000000000000111 ""
    
    • 可以看到 layout 值为空字符串,并没有展示出任何直观的布局信息,看来要想知道 layout 是怎么运作的,可以阅读 block 代码clang 代码,继续一步步地分析这两段代码里面隐藏的信息,这里贴出其中的部分代码和注释:
    // block
    // Extended layout encoding.
    
    // Values for Block_descriptor_3->layout with BLOCK_HAS_EXTENDED_LAYOUT
    // and for Block_byref_3->layout with BLOCK_BYREF_LAYOUT_EXTENDED
    
    // If the layout field is less than 0x1000, then it is a compact encoding 
    // of the form 0xXYZ: X strong pointers, then Y byref pointers, 
    // then Z weak pointers.
    
    // If the layout field is 0x1000 or greater, it points to a 
    // string of layout bytes. Each byte is of the form 0xPN.
    // Operator P is from the list below. Value N is a parameter for the operator.
    
    enum {
        ...
        BLOCK_LAYOUT_NON_OBJECT_BYTES = 1,    // N bytes non-objects
        BLOCK_LAYOUT_NON_OBJECT_WORDS = 2,    // N words non-objects
        BLOCK_LAYOUT_STRONG           = 3,    // N words strong pointers
        BLOCK_LAYOUT_BYREF            = 4,    // N words byref pointers
        BLOCK_LAYOUT_WEAK             = 5,    // N words weak pointers
        ...
    };
    
    // clang 
    /// InlineLayoutInstruction - This routine produce an inline instruction for the
    /// block variable layout if it can. If not, it returns 0. Rules are as follow:
    /// If ((uintptr_t) layout) < (1 << 12), the layout is inline. In the 64bit world,
    /// an inline layout of value 0x0000000000000xyz is interpreted as follows:
    /// x captured object pointers of BLOCK_LAYOUT_STRONG. Followed by
    /// y captured object of BLOCK_LAYOUT_BYREF. Followed by
    /// z captured object of BLOCK_LAYOUT_WEAK. If any of the above is missing, zero
    /// replaces it. For example, 0x00000x00 means x BLOCK_LAYOUT_STRONG and no
    /// BLOCK_LAYOUT_BYREF and no BLOCK_LAYOUT_WEAK objects are captured.
    
    • 首先要解释的是 inline 这个词,Objective-C 中有一种叫做 Tagged Pointer 的技术,它让指针保存实际值,而不是保存实际值的地址,这里的 inline 也是相同的效果,即让 layout 指针保存实际的编码信息。在 inline 状态下,使用十六进制中的一位表示捕获变量的数量,所以每种类型的变量最多只能有 15 个,此时的 layout 的值以 0xXYZ 形式呈现,其中 X、Y、Z 分别表示捕获 __strong、__block、__weak 修饰指针变量的个数,如果其中某个类型的数量超过 15 或者捕获变量的修饰类型不为这三种任何一个时,比如捕获的变量由 __unsafe_unretained 修饰,则采用另一种编码方式,这种方式下,layout 会指向一个字符串,这个字符串的每个字节以 0xPN 的形式呈现,并以 0x00 结束,P 表示变量类型,N 表示变量个数,需要注意的是,N 为 0 表示 P 类型有一个,而不是 0 个,也就是说实际的变量个数比 N 大 1。
    • 需要注意的是,捕获 int 等基础类型,不影响 layout 的呈现方式,layout 编码中也不会有关于基础类型的信息,除非需要基础类型的编码来辅助定位对象指针类型的位置,比如捕获含有对象指针字段的结构体。
    • 如下所示:代码块没有捕获任何对象指针,所以实际的 descriptor 不包含 copy 和 dispose 字段:
    unsigned long long j = 4;
    int i = 3;
    char c = 'a';
    void (^blk)(void) = ^{
        i;
        c;
        j;
    };
    
    • 去除这两个字段后,再输出实际的布局信息,结果为空(0x00 表示结束),说明捕获一般基础类型变量不会计入实际的 layout 编码:
    (lldb) p/x (long)layout->descriptor->layout
    (long) $0 = 0x0000000100001f67
    (lldb) x/8bx layout->descriptor->layout
    0x100001f67: 0x00 0x76 0x31 0x36 0x40 0x30 0x3a 0x38
    
    • 接着尝试第一种 layout 方式:
    NSObject *o1 = [NSObject new];
    __block NSObject *o3 = o1;
    __weak NSObject *o2 = o1;
    void (^blk)(void) = ^{
        o1;
        o2;
        o3;
    };
    
    • 以上代码块对应的 layout 值为 0x111,表示三种类型变量每种一个:
    (lldb) p/x (long)layout->descriptor->layout
    (long) $0 = 0x0000000000000111
    
    • 再尝试第二种 layout 编码方式:
    NSObject *o1 = [NSObject new];
    __block NSObject *o3 = o1;
    __weak NSObject *o2 = o1;
    NSObject *o4 = o1;
    ... // 5 - 18
    NSObject *o19 = o1;
    void (^blk)(void) = ^{
        o1;
        o2;
        o3;
        o4;
        ... // 5 - 18
        o19;
    };
    
    • 以上代码块对应的 layout 值是一个地址 0x0000000100002f44 ,这个地址为编码字符串的起始地址,转换成十六进制后为 0x3f 0x30 0x40 0x50 0x00,其中 P 为 3 表示 __strong 修饰的变量,数量为 15(f) + 1 + 0 + 1 = 17 个,P 为 4 表示 __block 修饰的变量,数量为 0 + 1 = 1 个, P 为 5 表示 __weak 修饰的变量,数量为 0 + 1 = 1 个:
    (lldb) p/x (long)layout->descriptor->layout
    (long) $0 = 0x0000000100002f44
    (lldb) x/8bx layout->descriptor->layout
    0x100002f44: 0x3f 0x30 0x40 0x50 0x00 0x76 0x31 0x36
    

    ④ 结构体对捕获布局的影响

    • 由于结构体字段的布局顺序在声明时就已经确定,无法像 block 构造捕获区域一样,按照变量类型、修饰符进行调整,所以如果结构体中有类型为对象指针的字段,就需要一些额外信息来计算这些对象指针字段的偏移量,需要注意的是,被捕获结构体的内存对齐信息和未捕获时一致,以寻址长度作为对齐基准,捕获操作并不会变更对齐信息。
    • 同样地,先尝试捕获只有基本类型字段的结构体:
    struct S {
        char c;
        int i;
        long j;
    } foo;
    void (^blk)(void) = ^{
      foo;
    };
    
    • 然后调整 descriptor 结构,输出 layout :
    (lldb) x/8bx layout->descriptor->layout
    0x100001f67: 0x00 0x76 0x31 0x36 0x40 0x30 0x3a 0x38
    
    • 可以看到,只有含有基本类型的结构体,同样不会影响 block 的 layout 编码信息。给结构体新增 __strong 和 __weak 修饰的对象指针字段:
    struct S {
        char c;
        int i;
        __strong NSObject *o1;
        long j;
        __weak NSObject *o2;
    } foo;
    void (^blk)(void) = ^{
      foo;
    };
    
    • 同样分析输出 layout :
    (lldb) x/8bx layout->descriptor->layout
    0x100002f47: 0x20 0x30 0x20 0x50 0x00 0x76 0x31 0x36
    
    • layout 编码为0x20 0x30 0x20 0x50 0x00,其中 P 为 2 表示 word 字类型(非对象),由于字大小一般和指针一致,所以表示占用 8 * (N + 1) 个字节,第一个 0x20 表示非对象指针类型占用了 8 个字节,也就是 char 类型和 int 类型字段对齐之后所占用的空间,接着 0x30 表示有一个 __strong 修饰的对象指针字段,第二个 0x20 表示非对象指针 long 类型占用 8 个字节,最后的 0x50 表示有一个 __weak 修饰的对象指针字段。由于编码中包含每个字段的排序和大小,就可以通过解析 layout 编码后的偏移量,拿到想要的对象指针值。 P 还有个 byte 类型,值为 1,和 word 类型有相似的功能,只是表示的空间大小不同。

    ⑤ Byref 结构的布局

    • 由 __block 修饰的捕获变量,会先转换成 byref 结构,再由这个结构去持有实际的捕获变量,block 只负责管理 byref 结构:
    // 标志位不一样,这个结构的实际布局也会有差别,简单地放在一起好阅读
    struct sr_block_byref {
        void *isa;
        struct sr_block_byref *forwarding;
        // contains ref count
        volatile int32_t flags; 
        uint32_t size;
        // requires BLOCK_BYREF_HAS_COPY_DISPOSE
        void (*byref_keep)(struct sr_block_byref *dst, struct sr_block_byref *src);
        void (*byref_destroy)(struct sr_block_byref *);
        // requires BLOCK_BYREF_LAYOUT_EXTENDED
        const char *layout;
    };
    
    • 以上代码块就是 byref 对应的结构体,第一眼看上去,比较困惑为什么还要有 layout 字段,虽然 block 源码注释说明 byref 和 block 结构一样,都具备两种不同的布局编码方式,但是 byref 不是只针对一个变量吗,难道和 block 捕获区域一样也可以携带多个捕获变量?带着这个困惑,先看下以下表达式 :
    __block  NSObject *o1 = [NSObject new];
    
    • 使用 clang 重写之后:
    struct __Block_byref_o1_0 {
        void *__isa;
        __Block_byref_o1_0 *__forwarding;
        int __flags;
        int __size;
        void (*__Block_byref_id_object_copy)(void*, void*);
        void (*__Block_byre/* @autoreleasepool */o{ __AtAutoreleasePool __autoreleasepool; e)(void*);
        NSObject *o1;
    };
    
    • 和 block 捕获变量一样,byref 携带的变量也是保存在结构体尾部的内存空间里,当前上下文中,可以直接通过 sr_block_byref 的 layout 字段获取 o1 对象指针值。可以看到,在包装如对象指针这类常规变量时,layout 字段并没有起到实质性的作用,那什么条件下的 layout 才表示布局编码信息呢?如果使用 layout 字段表示编码信息,那么携带的变量又是何处安放的呢?
    • 针对第一个问题,先看以下代码块 :
    __block struct S {
        NSObject *o1;
    } foo;
    foo.o1 = [NSObject new];
    void (^blk)(void) = ^{
      foo;
    };
    
    • 使用 clang 重写之后:
    struct __Block_byref_foo_0 {
      void *__isa;
      __Block_byref_foo_0 *__forwarding;
      int __flags;
      int __size;
      void (*__Block_byref_id_object_copy)(void*, void*);
      void (*__Block_byref_id_object_dispose)(void*);
      struct S foo;
    };
    
    • 和常规类型一样,foo 结构体保存在结构体尾部,也就是原本 layout 所在的字段,重写的代码中依然看不到 layout 的踪影,接着输出 foo :
    (lldb) po foo.o1
    <NSObject: 0x10061f130>
    (lldb) p (struct S)a_byref->layout
    error: Multiple internal symbols found for 'S'
    (lldb) p/x (long)a_byref->layout
    (long) $3 = 0x0000000000000100
    (lldb) x/56bx a_byref
    0x100627c20: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x100627c28: 0x20 0x7c 0x62 0x00 0x01 0x00 0x00 0x00
    0x100627c30: 0x04 0x00 0x00 0x13 0x38 0x00 0x00 0x00
    0x100627c38: 0x90 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x100627c40: 0x00 0x1c 0x00 0x00 0x01 0x00 0x00 0x00
    0x100627c48: 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00
    0x100627c50: 0x30 0xf1 0x61 0x00 0x01 0x00 0x00 0x00
    
    • 看来事情并没有看上去的那么简单,首先重写代码中 foo 字段所在内存保存的并不是结构体,而是 0x0000000000000100,这个 100 是不是看着有点眼熟?没错,这就是 byref 的 layout 信息,根据 0xXYZ 编码规则,这个值表示有 1 个 __strong 修饰的对象指针。
    • 接着针对第二个问题,携带的对象指针变量存在哪?往下移动 8 个字节,这不就是 foo.o1 对象指针的值么?总结下,在存在 layout 的情况下,byref 使用 8 个字节保存 layout 编码信息,并紧跟着在 layout 字段后存储捕获的变量。
    • 以上是 byref 的第一种 layout 编码方式,再尝试第二种:
    __block struct S {
        char c;
        NSObject *o1;
        __weak NSObject *o3;
    } foo;
    foo.o1 = [NSObject new];
    void (^blk)(void) = ^{
      foo;
    };
    
    • 使用 clang 重写代码之后 :
    struct __Block_byref_foo_0 {
      void *__isa;
    __Block_byref_foo_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*/* @autoreleasepool */c{ __AtAutoreleasePool __autoreleasepool; _byref
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    • 上面代码并不是粘贴错误,貌似 Rewriter 并不能很好地处理这种情况,看来又需要直接去看对应内存地址中的值:
    (lldb) x/72bx a_byref
    0x100755140: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x100755148: 0x40 0x51 0x75 0x00 0x01 0x00 0x00 0x00
    0x100755150: 0x04 0x00 0x00 0x13 0x48 0x00 0x00 0x00
    0x100755158: 0x10 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x100755160: 0xa0 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x100755168: 0x8d 0x3e 0x00 0x00 0x01 0x00 0x00 0x00
    0x100755170: 0x00 0x5f 0x6b 0x65 0x79 0x00 0x00 0x00
    0x100755178: 0xd0 0x6e 0x75 0x00 0x01 0x00 0x00 0x00
    0x100755180: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    (lldb) x/8bx a_byref->layout
    0x100003e8d: 0x20 0x30 0x50 0x00 0x53 0x52 0x4c 0x61
    
    • 地址 0x100755168 中保存 layout 编码字符串的地址 0x0000000100003e8d ,将此字符串转换成十六进制后为 0x20 0x30 0x50 0x00。

    ⑥ 强引用对象的获取

    • 已经知道 block / byref 如何布局捕获区域内存,以及如何获取关键的布局信息,接下来就可以尝试获取 block 强引用的对象,强引用的对象可以分成两部分:
      • 被 block 强引用;
      • 被 byref 结构强引用。
    • 只要获取这两部分强引用的对象就可以了,由于上文已经将整个原理脉络理清,所以编写出可用的代码并不困难。这两部分都涉及到布局编码,先根据 layout 的编码方式,解析出捕获变量的类型和数量:
    SRCapturedLayoutInfo *info = [SRCapturedLayoutInfo new];
        
    if ((uintptr_t)layout < (1 << 12)) {
        uintptr_t inlineLayout = (uintptr_t)layout;
        [info addItemWithType:SR_BLOCK_LAYOUT_STRONG count:(inlineLayout & 0xf00) >> 8];
        [info addItemWithType:SR_BLOCK_LAYOUT_BYREF count:(inlineLayout & 0xf0) >> 4];
        [info addItemWithType:SR_BLOCK_LAYOUT_WEAK count:inlineLayout & 0xf];
    } else {
        while (layout && *layout != '\x00') {
            unsigned int type = (*layout & 0xf0) >> 4;
            unsigned int count = (*layout & 0xf) + 1;
            
            [info addItemWithType:type count:count];
            layout++;
        }
    }
    
    • 然后遍历 block 的布局编码信息,根据变量类型和数量,计算出对象指针地址偏移,获取对应的对象指针值:
    - (NSHashTable *)strongReferencesForBlockLayout:(void *)iLayout {
        if (!iLayout) return nil;
        
        struct sr_block_layout *aLayout = (struct sr_block_layout *)iLayout;
        const char *extenedLayout = sr_block_extended_layout(aLayout);
        _blockLayoutInfo = [SRCapturedLayoutInfo infoForLayoutEncode:extenedLayout];
        
        NSHashTable *references = [NSHashTable weakObjectsHashTable];
        uintptr_t *begin = (uintptr_t *)aLayout->captured;
        for (SRLayoutItem *item in _blockLayoutInfo.layoutItems) {
            switch (item.type) {
                case SR_BLOCK_LAYOUT_STRONG: {
                    NSHashTable *objects = [item objectsForBeginAddress:begin];
                    SRAddObjectsFromHashTable(references, objects);
                    begin += item.count;
                } break;
                case SR_BLOCK_LAYOUT_BYREF: {
                    for (int i = 0; i < item.count; i++, begin++) {
                        struct sr_block_byref *aByref = *(struct sr_block_byref **)begin;
                        NSHashTable *objects = [self strongReferenceForBlockByref:aByref];
                        SRAddObjectsFromHashTable(references, objects);
                    }
                } break;
                case SR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {
                    begin = (uintptr_t *)((uintptr_t)begin + item.count);
                } break;
                default: {
                    begin += item.count;
                } break;
            }
        }
        return references;
    }
    
    • block 布局区域中的 byref 结构需要进行额外的处理,如果 byref 直接携带 __strong 修饰的变量,则不需要关心 layout 编码,直接从结构尾部获取指针变量值即可,否则需要和处理 block 布局区域一样,先得到布局信息,然后遍历这些布局信息,计算偏移量,获取强引用对象地址:
    - (NSHashTable *)strongReferenceForBlockByref:(void *)iByref {
        if (!iByref) return nil;
        
        struct sr_block_byref *aByref = (struct sr_block_byref *)iByref;
        NSHashTable *references = [NSHashTable weakObjectsHashTable];
        int32_t flag = aByref->flags & SR_BLOCK_BYREF_LAYOUT_MASK;
        
        switch (flag) {
            case SR_BLOCK_BYREF_LAYOUT_STRONG: {
                void **begin = sr_block_byref_captured(aByref);
                id object = (__bridge id _Nonnull)(*(void **)begin);
                if (object) [references addObject:object];
            } break;
            case SR_BLOCK_BYREF_LAYOUT_EXTENDED: {
                const char *layout = sr_block_byref_extended_layout(aByref);
                SRCapturedLayoutInfo *info = [SRCapturedLayoutInfo infoForLayoutEncode:layout];
                [_blockByrefLayoutInfos addObject:info];
                
                uintptr_t *begin = (uintptr_t *)sr_block_byref_captured(aByref) + 1;
                for (SRLayoutItem *item in info.layoutItems) {
                    switch (item.type) {
                        case SR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {
                            begin = (uintptr_t *)((uintptr_t)begin + item.count);
                        } break;
                        case SR_BLOCK_LAYOUT_STRONG: {
                            NSHashTable *objects = [item objectsForBeginAddress:begin];
                            SRAddObjectsFromHashTable(references, objects);
                            begin += item.count;
                        } break;
                        default: {
                            begin += item.count;
                        } break;
                    }
                }
            } break;
            default: break;
        }
        return references;
    }
    

    ⑦ 另一种强引用对象获取方式

    • 上文通过将 block 的布局编码信息转化为对应字段的偏移量来获取强引用对象,还有另外一种比较取巧的方式,也是目前检测循环引用工具获取 block 强引用对象的常用方式,比如 facebook 的 FBRetainCycleDetector
    • 根据 FBRetainCycleDetector 对应的源码,此方式大致原理如下:
      • 获取 block 的 dispose 函数 (如果捕获了强引用对象,需要利用这个函数解引用);
      • 构造一个 fake 对象,此对象由若干个扩展的 byref 结构 (对象) 组成,其个数由 block size 决定,即把 block 划分为若干个 8 字节内存区域,就像以下代码块一样 :
    struct S {
        NSObject *o1;
        NSObject *o2;
    };
    struct S s = {
        .o2 = [NSObject new]
    };
    void **fake = (void **)&s;
    // fake[1] 和 s.o2 是一样的
    
      • 扩展的 byref 结构会重写 release 方法,只在此方法中设置强引用标识位,不执行原释放逻辑;
      • 将 fake 对象作为参数,调用 dispose 函数,dispose 函数会去 release 每个 block 强引用的对象,这些强引用对象被替换成 byref 结构,所以可以通过它的强引用标识位判断 block 的哪块区域保存了强引用对象地址;
      • 遍历 fake 对象,保存所有强引用标志位被设置的 byref 结构对应索引,通过这个索引可以去 block 中找强引用指针地址;
      • 释放所有的 byref 结构;
      • 根据上面得到的索引,获取捕获变量偏移量,偏移量为索引值 * 8 字节 (指针大小) ,再根据偏移量去 block 内存块中拿强引用对象地址。
    • 关于这种方案,需要明确:
      • 首先这种方案也需要在明确 block 内存布局的情况下才能够实施,因为 block ,或者说 block 结构体,实际执行内存对齐时,并没有按照寻址大小也就是 8 字节对齐,假设 block 捕获区域的对齐方式变成如下的这样 :
    struct __main_block_impl_0 {
      struct __block_impl impl;           // 24
      struct __main_block_desc_0* Desc;   // 8 指针占用内存大小和寻址长度相关,在 64 位机环境下,编译器分配空间大小为 8 字节
      int i;                              // 4    FakedByref 8
      NSObject *o1;                       // 8    FakedByref 8 [这里上个 FakedByref 后 4 个子节和当前 FakedByref 前 4 字节覆盖 o1 对象指针的 8 字节,导致 miss ]
      char c;                             // 1
      NSObject *o2;                       // 8
    }
    
      • 那么使用 fake 的方案就会失效,因为这种方案的前提是 block 内存对齐基准基于寻址长度,即指针大小。不过 block 对捕获的变量按照类型和尺寸进行了排序,__strong 修饰的对象指针都在前面,本来只需要这种类型的变量,并不关心其它类型,所以即使后面的对齐方式不满足 fake 条件也没关系,另外捕获结构体的对齐基准是基于寻址长度的,即使结构体有其他类型,也满足 fake 条件 :
    struct __main_block_impl_0 {
      struct __block_impl impl;           // 24
      struct __main_block_desc_0* Desc;   // 8 指针占用内存大小和寻址长度相关,在 64 位机环境下,编译器分配空间大小为 8 字节
      NSObject *o1;                       // 8    FakedByref 8
      NSObject *o2;                       // 8    FakedByref 8
      int i;                              // 4    FakedByref 8
      char c;                             // 1        
    }
    
      • 可以看到,通过以上代码块的排序,让 o1 和 o2 都被 FakedByref 结构覆盖,而 i、c 变量本身就不会在 dispose 函数中访问,因此怎么设置都不会影响到策略的生效;
      • 第二点是为什么要用扩展的 byref 结构,而不是随便整个重写 release 的类,这是因为当 block 捕获了 __block 修饰的指针变量时,会将这个指针变量包装成 byref 结构,而 dispose 函数会对这个 byref 结构执行 _Block_object_dispose 操作,这个函数有两个形参,一个是对象指针,一个是 flag,当 flag 指明对象指针为 byref 类型,而实际传入的实参不是,就会出现问题,所以必须用扩展的 byref 结构;
      • 第三点是这种方式无法处理 __block 修饰对象指针的情况。
    • 不过这种方式贵在简洁,无需考虑内部每种变量类型具体的布局方式,就可以满足大部分需要获取 block 强引用对象的场景。

    三、对象成员变量强引用

    • 对象强引用成员变量的获取相对来说直接些,因为每个对象对应的类中都有其成员变量的布局信息,并且 runtime 有现成的接口,只需要分析出编码格式,然后按顺序和成员变量匹配即可。获取编码信息的接口有两个, class_getIvarLayout 函数返回描述 strong ivar 数量和索引信的编码信息,相对的 class_getWeakIvarLayout 函数返回描述 weak ivar 的编码信息。
    • class_getIvarLayout 返回值是一个 uint8 指针,指向一个字符串,uint8 在 16 进制下占用 2 位,所以编码以 2 位为一组,组内首位描述非 strong ivar 个数,次位为 strong ivar 个数,最后一组如果 strong ivar 个数为 0,则忽略,且 layout 以 0x00 结尾。
    • 如下所示:
    // 0x0100
    @interface A : NSObject {
        __strong NSObject *s1;
    }
    @end
    
    • 起始非 strong ivar 个数为 0,并且接着一个 strong ivar ,得出编码为 0x01 。
    // 0x0100
    @interface A : NSObject {
        __strong NSObject *s1;
        __weak NSObject *w1;
    }
    @end
    
    • 起始非 strong ivar 个数为 0,并且接着一个 strong ivar ,得出编码为 0x01,接着有个 weak ivar,但是后面没有 strong ivar,所以忽略。
    // 0x011100
    @interface A : NSObject {
        __strong NSObject *s1;
        __weak NSObject *w1;
        __strong NSObject *s2;
    }
    @end
    
    • 起始非 strong ivar 个数为 0,并且接着一个 strong ivar ,得出编码为 0x01,接着有个 weak ivar,并且后面紧接着一个 strong ivar ,得出编码 0x11 ,合并得到 0x0111。
    // 0x211100
    @interface A : NSObject {
        int i1;
        void *p1;
        __strong NSObject *s1;
        __weak NSObject *w1;
        __strong NSObject *s2;
    }
    @end
    
    • 起始非 strong ivar 个数为 2,并且紧接着一个 strong ivar,得出编码 0x21,接着有个 weak ivar,后面紧接着一个 strong ivar ,得出编码 0x11 ,合并得到 0x2111。
    • 了解了成员变量的编码格式,剩下的就是如何解码并依次和成员变量进行匹配, FBRetainCycleDetector 已经实现了这部分功能 ,主要原理如下:
      • 获取所有的成员变量以及 ivar 编码;
      • 解析 ivar 编码,跳过非 strong ivar ,获得 strong ivar 所在索引值 (把对象分成若干个 8 字节内存片段);
      • 利用 ivar_getOffset 函数获取 ivar 的偏移量,除以指针大小就是自身的索引值 (对象布局对齐基准为寻址长度,这里为 8 字节);
      • 匹配 2、3 步获得的索引值,得到 strong ivar;
      • 实现了对结构体的处理。

    四、总结

    • “Block 捕获实体引用”和“对象成员变量强引用”是检测循环引用两个比较关键的点,特别是获取 block 捕获的强引用对象环节,block ABI 中并没有详细说明捕获区域布局信息,需要自己结合 block 源码以及 clang 生成 block 的 CodeGen 逻辑去推测实际的布局信息。
    展开全文
  • 【C语言】指针和引用的区别

    千次阅读 2019-06-11 11:40:34
    指针和引用的区别 1.指针是一个实体,而引用是一个别名;在汇编上,引用的底层是以指针的方式实现的,定义一个引用变量,相当于就是定义了一个指针,然后把引用内存的地址写到这个指针里面,当通过引用变量修改它...
  • 引用、指针句柄的区别

    万次阅读 多人点赞 2020-12-15 08:38:00
    句柄是一种特殊的智能指针 。当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。句柄与普通指针的区别在于,指针包含的是引用对象的内存地址,而句柄则是由...
  • 掩码(BitMask)——介绍与使用

    千次阅读 多人点赞 2019-06-30 20:02:49
    今天在群里看到有人讨论小白鼠试毒药的问题,感觉很有意思,生活中处处都有技术解决问题的思想,思想的提高也会带动技术水平的提高吧,转自一网友的帖子,仅记录学习应用,多谢。 一、前言 运算在我们实际开发...
  • 值类型和引用类型的区别

    万次阅读 2017-10-25 10:07:08
    一、定义引用类型表示你操作的数据是同一个,也就是说当你传一个参数给另一个方法时,你在另一个方法中改变这个变量的值,那么调用这个方法是传入的变量的值也将改变。值类型表示复制一个当前变量传给方法,当你在这...
  • 《STM32中文参考手册V10》-第8章通用复用功能IO(GPIOAFIO) 《Cortex-M3权威指南(中文)》第5章 带操作   硬件连接 假设跑马灯实验的硬件连接如上图所示,LED0连接PB5,LED1连接PE5。由于在LED的另一端是...
  • C/C++引用和指针的区别

    万次阅读 多人点赞 2019-05-28 21:57:58
    为什么C/C++语言使用指针?...C++将指针暴露给了用户(程序员),而JavaC#等语言则将指针隐藏起来了。 “Everything uses pointers. C++ just exposes them rather than hiding them,” It's easier to gi...
  • JVM虚拟机32位和64位有什么不同

    千次阅读 2018-03-06 11:56:48
    JVM虚拟机32位和64位的探索其实就是因为操作系统有32位和64位,这两者有什么区别呢?引用链接 http://blog.sina.com.cn/s/blog_4adc4b090102vr3a.html所谓32位处理器就是一次只能处理32位,也就是4个字节的数据,而...
  • 引用与指针的区别

    千次阅读 多人点赞 2021-06-30 20:17:19
    在sizeof中含义不同: 引用结果为引用类型的大小,但指针始终是地址空间,所占字节个数(32平台占4个字节) 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小 有多级指针,但没有多级引用 访问实体的...
  • 解决办法如下 1、在IDE中将目标平台设置成x86(VS是在项目的属性-&gt;生成-&gt;目标平台) 2、如果DLL中调用了其他的DLL,需要将其他的DLL一同编译 3、有时DLL生成时会依赖于IDE,比如BCB下生成的DLL就会...
  • 浅谈C++中引用和指针的区别

    千次阅读 2019-04-15 02:00:13
    之前我们介绍了什么是引用,错过的小伙伴们可以戳这里 ↓ ...既然引用底层是用指针形式实现的,那么这篇文章就来为大家介绍一下引用和指针的区别: int main() { int a = 10; int& ra = a; ra = 20; int...
  • 页表项中各个的作用

    万次阅读 2018-07-30 21:09:50
    |页号|物理块号|状态P|访问字段A|修改M|外存地址| 各个的作用: 1)状态P:用于指示该页是否已调入内存,供程序访问时参考; 2)访问字段A:用于记录本页在一段时间内被访问次数,供选择换出页面时参考;...
  • 欢迎使用Markdown编辑器写博客本Markdown编辑器使用[StackEdit][6]修改而来,用它写博客,将会带来全新的体验哦: Markdown扩展Markdown简洁的语法 代码块高亮 图片链接图片上传 LaTex数学公式 ...引用
  • 深剖C++内联函数和引用机制

    千次阅读 多人点赞 2022-05-09 22:19:57
    带你一命通关C++基础部分 五分钟手撕内联函数 单手吊打引用机制
  • [Java JVM] Hotspot GC研究- 开篇&对象内存布局 中介绍对象内存布局时, 曾提到过, 由于在64CPU下, 指针的宽度是64的, 而实际的heap区域远远用不到这么大的内存, 使用64bit来存对象引用会造成浪费, 所以应该做点...
  • 二进制运算

    万次阅读 多人点赞 2017-12-03 20:00:01
    尤其是安全底层开发中,除了指针的频繁使用之外,运算是另一个非常频繁使用的领域。 因此,在求职面试中,运算也是每年重点考查的知识点。首先,我们有必要复习一下C语言中运算的一些基础计
  • 吃人的那些 Java 名词:对象、引用、堆、栈

    万次阅读 多人点赞 2019-09-05 15:57:09
    作为一个有着 8 年 Java 编程经验的 IT 老兵,说起来很惭愧,我被 Java 当中的四五个名词一直困扰着:**对象、引用、堆、栈、堆栈**(栈可同堆栈,因此是四个名词,也是五个名词)。每次我看到这几个名词,都隐隐...
  • 运算--

    千次阅读 2022-01-07 16:47:46
    一、数字系统、位、字节 1.数字系统(进位计数制) 1)数码:表示数值大小不同的符号。 例如十进制有10个数码,二进制有2个数码,十六进制有16个数码 2)基数:数制所使用数码的...2.位和字节(表示信息的单位) 1)
  • 【js中的基本类型和引用类型有哪些,有什么区别?】 每篇分享文从 【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】 八个方面深度解析前端知识/技能,本篇...
  • c语言中变量的引用传递指针

    万次阅读 多人点赞 2017-08-02 19:25:15
    掌握了引用型变量指针,才能深入掌握面向过程的函数调用机制。 引用型变量存储的是变量的地址,指针存储的也是变量的地址,所以本质上来说二者是一样的。 使用引用型变量,子函数中所有的操作会直接修改主函数中的...
  • 32寄存器用法介绍

    千次阅读 2020-11-12 13:58:16
    8个32通用寄存器,其中包含4个数据寄存器(EAX、EBX、ECX、EDX)、2个变址寄存器(ESIEDI)2个指针寄存器(ESPEBP) 6个段寄存器(ES、CS、SS、DS、FS、GS) 1个指令指针寄存器(EIP) 1个标志寄存器...
  • 32位和64位系统对于程序员的影响

    千次阅读 2015-05-19 16:47:02
    语言编程需要注意的64位和32机器的区别 一、数据类型特别是int相关的类型在不同位数机器的平台下长度不同。C99标准并不规定具体数据类型的长度大小,只规定级别。作下比较: 16位平台 char 1个字节8位 short 2个...
  • 32位和64位编程注意事项总结

    千次阅读 2016-11-02 11:45:35
    这行代码将会在32位和64位系统上都运行正常。 其他有关于对常量硬编码的问题,都是基于对ILP32数据模型的不当认识,如下: int **p; p = (int**)malloc(4 * NO_ELEMENTS); ...
  • 32位和64位及其内存长度

    万次阅读 2016-10-17 15:25:22
    一)64系统32有什么区别?  1、64bit CPU拥有更大的寻址能力,最大支持到16GB内存,而32bit只支持4G内存 2、64CPU一次可提取64数据,比32提高了一倍,理论上性能会提升1倍。但这是建立在64bit...
  • General-Purpose Registers in 64-mode 在64模式下,有16个64位宽通用寄存器(32模式是8个...如果指定了32操作数,则可以使用EAX/EBX/ECX/EDX/EBP/ESP/ESI/EDI新增的R8D~R15D(注:这里D表示Doubledor...
  • 所谓平台也就是指硬件与相应的系统软件(包括操作系统、编译器与开发环境有关的应用程序(如数据库))。  64硬件体系结构是指: (1).能处理64数据.---即CPU可以将64数据作为基本单元进行处理(只需一次操作就...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 350,122
精华内容 140,048
热门标签
关键字:

引用位和访问位

友情链接: sjq.zip