windows PE 指南(基础部分)(一)

查找软件注册码

你好!


64位汇编和编译连接

//Hello.c
#include <stdio.h>
int add(int, int);
int main()
{
    
    
	printf("Hello World %d\n", add(1, 2));
	getchar();
	return 0;
}

//Hello1.c
int add(int a, int b)
{
    
    
	return a + b;
}

我们打开vs2019命令行工具 x64 Native Tools Command Prompt for VS 2019

cl /c hello.c
cl /c hello1.c
link hello.obj hello1.obj
  • 在vs中鼠标右击项目属性,Build Dependencies,Build Customizations…
  • 选择masm(.target, .props)
  • 添加源文件1.asm
//1.asm

; Sample x64 Assembly Program
; Chris Lomont 2009 www.lomont.org

extrn ExitProcess: PROC 	; external functions in system libraries
extrn MessageBoxA: PROC

;64位没有 .model 宏指令,不能指定内存模型和调用约定

.data
	caption db '64-bit hello!', 0
	message db 'Hello World!', 0
.code
	Start PROC
		;函数调用前需要预留影子空间,对其rsp
		sub rsp, 18h	; shadow space, aligns stack	没有这行代码,程序运行就会崩溃,抛出异常:0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF
		mov rcx, 0	; hwnd = HWND_DESKTOP
		lea rdx, message	; LPCSTR lpText
		lea r8, caption	; LPCSTR lpCaption
		mov r9d, 0	; uType = MB_OK
		;函数调用使用fastcall 
		call MessageBoxA	; call MessageBox API function
		mov ecx, eax	; uExitCode = MessageBox(...)
		call ExitProcess	; 执行了这行代码,整个程序就会结束,后面的其实随便填写代码,都不会执行
		add rsp, 28h	; 每个call压入8个字节的返回地址
	Start ENDP	;以后直接end,不用指明符号
End

(如果编译没反应的话,请鼠标右击1.asm文件,配置属性、常规,设置从生成中排除为否;
选择项类型为自定义生成工具,依次点击应用;
这时候左侧就多了一项自定义生成工具,点击确定;
重新右击1.asm,配置属性、常规,出现如下图所示Microsoft Macro Assembler,设置从生成中排除为否)

在这里插入图片描述

F7编译,编译错误提示:LINK: error LNK2001: unresolved external symbol mainCRTStartup
解决:项目属性,Linker,Advanced,在Entry Point填入Start即可。

如果编译的时候提示错误:
asm error A2008: syntax error : in instruction
error A2008: syntax error : endp
解决:
请仔细检查汇编代码,逗号、分号都要确保是英文输入;把每个换行删除,重新再按一遍回车;
为了避免编码问题,把整个汇编代码复制拷贝到记事本,在复制粘贴回来;或者复制到notepad++,修改编码为ANSI。

sub rsp, 18h ; shadow space, aligns stack

现在我们来看上面这句汇编代码的作用:
没有这行代码,程序运行就会崩溃,抛出异常:0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF。

解释:
函数开始时候的栈必须16位对齐,我们这个64位汇编写的函数其实也是被操作系统通过call指令调用的,会把函数的返回地址压入到栈里面,此时至少要给这个地址补齐8个字节,也就是至少这行代码为sub, rsp, 8h,此时在编译运行就没问题了;但是一般我们会在这个地方多给它搞出几个字节,叫shadow space,是为了不改某些32位下的API函数而做的“偷懒”办法,导致了我们在64位下必须把rcx、rdx、r8、r9传递过去的参数放到栈里面,即要把寄存器参数转换为32位的栈参数,所以为了稳定性,在写64位汇编程序的时候在开头要分配shadow space影子空间。

ml64 /c 1.asm
link 1.obj

链接的时候报错:error LNK2019: unresolved external symbol ExitProcess referenced in function main
error LNK2019: unresolved external symbol MessageBoxA referenced in function main
error LNK2001: unresolved external symbol mainCRTStartup

解决办法:我们在命令行里包含kernel32.lib和user32.lib即可。

link 1.obj /defaultlib:user32.lib /defaultlib:kernel32.lib /entry:Start

PE和COFF文件简介

PE和COFF文件布局简介

源代码的作用

常见的找不到符号的链接错误,是指链接器找不到函数名对应的可执行代码在什么地方,在源代码里面没有告诉编译器、链接器足够的信息;所以要在源代码里面,将这个函数名所在的lib文件所在位置放进去,总之这个静态库里面有这个函数的全部信息,有这个函数的可执行代码;如果这个lib只是一个跟dll相对应的符号导入的库的话,这个库有这个函数的可执行代码在哪个地方的信息,当你把这个lib放到我们这个源代码里面的时候,那么我们这个编译器看见这个函数名,它就会通过这个lib知道了这个函数在哪个地方哪个位置,将来它就会把这个信息合并到.obj和.exe里面,这样的话编译器就不会报错了。

obj的作用

好几个obj合并成一个exe,所以说obj里面的信息都要为编译器提供一些信息,这些信息被编译器用来合成我们的exe文件;所有的obj里面的这些信息都是为了obj合并成(或者说链接成)exe这个目的来服务的。

PE的作用

因为大部分exe是为了生成一个进程进行执行的,那么PE这种格式里面的所有信息都是为了,将这个exe装到内存里面然后执行这一个目的来服务的。
你可以想一想这个exe是怎么装到内存里面,是如何去执行的?

COFF格式文件布局概览

在这里插入图片描述

COFF格式包含上图这4个部分,第一部分的_IMAGE_FILE_HEADER跟PE格式中的标准PE头的一样;
第二部分的节头(节表)相当于一个变长数组,每个元素大小固定,用于描述各个节区(.text、.data等)的信息;

在这里插入图片描述

在节头(节表)这部分后面是符号Symbol和字符串String,符号Symbol后面紧跟着字符串String。

PE格式文件布局概览

在这里插入图片描述

_IMAGE_DOS_HEADER是用来兼容DOS系统,DOS系统下的可执行文件也有这个结构体,DOS系统的可执行文件中该结构体后面紧接着是可执行代码,所以,当一个windows系统下的PE可执行文件拿到DOS系统下去运行的时候,也会执行一段程序(LINK的时候附加进去的一段默认代码),用来提示用户一些错误信息。

在这里插入图片描述

_IMAGE_OPTIONAL_HEADER在我们COFF格式(或者说.obj文件)中是没有的。

在这里插入图片描述

_IMAGE_SECTION_HEADER和COFF格式里面是一模一样的,包括后面的各个Section_Data,以及Symbol和String(如果有的话)。

PE格式文件比COFF格式多了一个DOS头(_IMAGE_DOS_HEADER)和可选头(_IMAGE_OPTIONAL_HEADER),其他的都一样。

在这里插入图片描述

_IMAGE_OPTIONAL_HEADER中的信息跟我们的COFF格式(或者说.obj文件)没有关系,.obj没有必要提供ImageBase这类信息,这些信息是由LINK生成EXE可执行程序的时候可以自动生成的,所有这些信息都是我们PE所特有的,所以说可选头在我们COFF格式里面就没有必要,这些信息全是为了EXE文件怎么样装入内存里面运行所服务的。

COFF格式和PE格式的布局总结

结构体 COFF格式 PE格式
_IMAGE_DOS_HEADER 没有
_IMAGE_FILE_HEADER
_IMAGE_OPTIONAL_HEADER 没有
_IMAGE_SECTION_HEADER
Section_Data
Symbol 可选 可选
String 可选 可选

COFF头

COFF File Header

链接:COFF File Header (Object and Image)

  • Machine Types
    表示PE文件的运行平台。
    IMAGE_FILE_MACHINE_AMD64 : 0x8664 : x64
    IMAGE_FILE_MACHINE_I386 : 0x14c : Intel 386 or later processors and compatible processors
    IMAGE_FILE_MACHINE_IA64 : 0x200 : Intel Itanium processor family
  • Characteristics
    表示文件属性。
    IMAGE_FILE_LARGE_ADDRESS_ AWARE : 0x0020 : Application can handle > 2-GB addresses.
    应用程序可以处理大于2GB的地址空间,这种特征只对32位应用程序有用,对于64位应用程序没有用了。
    IMAGE_FILE_SYSTEM : 0x1000 : The image file is a system file, not a user program.
    驱动文件就具有这种特征,因为驱动是系统文件,不属于应用程序。

Section Header

链接:Section Table (Section Headers)

在这里插入图片描述

_IMAGE_SECTION_HEADER

  • Name
    An 8-byte, null-padded UTF-8 encoded string. If the string is exactly 8 characters long, there is no terminating null. For longer names, this field contains a slash (/) that is followed by an ASCII representation of a decimal number that is an offset into the string table. Executable images do not use a string table and do not support section names longer than 8 characters. Long names in object files are truncated if they are emitted to an executable file.
    PE格式不支持超过8个字节的节名称;可执行映像不使用字符串表,也不支持长度超过8个字符的节名称。
    COFF格式支持较长的名称,如果超过8个字节,此字段包含一个斜线(/),后面跟着一个十进制数的ASCII表示,该十进制数是字符串表的偏移量。
    如果.obj文件具有较长的节名称,在合并到.exe文件的时候会被截断。
    Name是8字节的节区名称,UTF-8字符;节区名称并不规定以零结尾,例如节区名称可以正好是8字节。

  • VirtualSize
    VirtualSize是这个节装到内存里面的大小(表示节区的大小,没有进行文件对齐的实际大小);
    VirtualSize和VirtualAddress对于我们COFF来说都是0,因为我们.obj文件不可能被装入到内存里面,它只能被链接器链接成PE格式的文件。

  • SizeOfRawData
    这个节在磁盘文件中的原始大小。
    表示节区的大小(基于文件对齐后的大小),该字段的值等于VirtualSize字段的值按照IMAGE_OPTIONAL_HEADER32.FileAlignment字段的值对齐以后的大小。

  • PointerToRawData
    这个节在.obj文件里面的偏移位置(节区的文件偏移地址FOA)。

  • PointerToRelocations
    这个是和重定位符号相关的,该指针指向的是一个数组,数组里面基本上每个元素对应着一个符号的重定位信息(偏移量),即该符号在.obj文件对应节中的偏移量。
    在.obj文件中使用,指向重定位表的指针。

  • NumberOfRelocations
    这个是重定位信息的个数(重定位条目的个数)。
    在.obj文件中使用,表示重定位表中元素的个数。

  • Characteristics
    这个节的属性(IMAGE_SCN_MEM_EXECUTE、IMAGE_SCN_MEM_READ、IMAGE_SCN_MEM_WRITE、IMAGE_SCN_MEM_SHARED、IMAGE_SCN_MEM_NOT_PAGED)。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

链接:Section Flags

Section Flags包含IMAGE_SCN_TYPE_NO_PAD(已过时、被IMAGE_SCN_ALIGN_1BYTE标志替代)、IMAGE_SCN_CNT_CODE(这个节包含可执行代码)、IMAGE_SCN_CNT_INITIALIZED_DATA(这个节包含已初始化的数据)、IMAGE_SCN_CNT_UNINITIALIZED_ DATA(这个节包含未初始化的数据)、IMAGE_SCN_LNK_INFO(.drectve节具有该类型)、IMAGE_SCN_MEM_NOT_CACHED(这个节不能放在缓存里面)、IMAGE_SCN_MEM_NOT_PAGED(这个节不能放在分页文件里面,这个分页或非分页是对整个节来说的,如果这个节有这个标志的话,那么这个节所有的代码都可以放在一个非分页内存里面)、IMAGE_SCN_MEM_SHARED(在内存中这个节可以被多个进程共享,共享内存就是通过一个节来共享内存的)、IMAGE_SCN_MEM_EXECUTE(这个节是一段可执行代码、节区包含可执行属性)、IMAGE_SCN_MEM_READ(这个节是可以读的、节区包含可读属性)、IMAGE_SCN_MEM_WRITE(这个节是可以写的、节区包含可写属性)。

如果你想找具有指定标志的节的话,例如IMAGE_SCN_GPREL标志(节区包含通过全局指针引用的数据),可以如下修改代码:

在这里插入图片描述


符号表一

链接:Other Contents of the File

在这里插入图片描述

在这里插入图片描述

我们知道了符号表在内存里面的位置(PointerToSymbolTable + p),也知道符号表中的元素数量(NumberOfSymbols),就可以得到紧跟在符号表后面的字符串表。
注意:符号表里面的每个元素并不一定代表一个符号(并不是一对一的关系),有时候是多个元素代表一个符号。

链接:符号表的格式

在这里插入图片描述

上图是符号表里面符号的标准格式,如果符号表里面的一个元素它的格式是这种的话,那么它就代表一个符号;
还有一些具有辅助作用的符号元素的格式。
链接:Auxiliary Symbol Records
它们有什么区别呢?标准格式的大小是18个字节,我们这个辅助元素也是18个字节,但是它的成员变量变了,也就是它的数据所代表的含义变了。

标准符号格式

符号表是一个记录数组,每条记录长18字节。每个记录都是标准的或辅助的符号表记录。标准记录定义了一个符号或一个名称,格式如下。

  • Name是这个符号的名字,比如说全局变量、静态变量、函数名等;
    符号表中的 ShortName 字段包含8个字节,其中包含名称本身(如果长度不超过8个字节) ,或者 ShortName 字段为字符串表提供偏移量。若要确定是给定名称本身还是偏移量,请测试前4个字节是否等于零。
    按照约定,这些名称被视为以零结尾的 UTF-8编码的字符串。

在这里插入图片描述

ShortName:一个8字节的数组。如果名称长度小于8个字节,则在右侧用空值填充该数组。
Zeores:如果名称长于8个字节,则该字段设置为全零。
Offset:字符串表中的偏移量。

  • Value根据符号的类型具有不同的含义,一般是个偏移量;

  • SectionNumber这个要重点说一下:
    我们知道结构_IMAGE_FILE_HEADER中的PointerToSymbolTable成员指向符号表,可以把它当作一个数组,索引(编号)是从0开始的;
    并且结构_IMAGE_FILE_HEADER中的NumberOfSymbols成员,是指符号表中符号的个数。
    如果符号表中的Name代表全局变量的话,比方说它的名字代表a,它的值Value一般来说是个偏移量,那么SectionNumber就指的我们这个全局变量所在节的编号,SectionNumber是以1为起始编号的,比如SectionNumber的值为5,那就是在我们第4个节里面,因为节的编号是从0开始的,但是SectionNumber这个地方的数值会是5,因为这个地方的编号是从1开始的(one-based index 基于1的索引)。
    SectionNumber是个标识节的有符号整数,使用基于1的索引“标识”节表(节表IMAGE_SECTION_TABLE是个数组,SectionNumber-1就是节表数组的索引,因为节表数组的索引是从0开始的,所以SectionNumber需要减1)。
    SectionNumber是个有符号数,可以为0和负数,当它的值为0的话说明这个符号不在这个节里面(尚未为符号记录分配节。值为零表示这个符号是对在别处定义的外部符号的引用,说明这个符号是一个外部引用变量。Value为非零值说明它是一个通用符号,其大小由Value指定。),同时Value成员的值也为0就表明它引用了在外部定义的一个符号,比方说我们在Souce.c里面定义了int yyy = 222;这个全局变量,那么在另一个源文件1.c中怎么使用它呢?就得extern int yyy;这样引用它一下就能使用这个变量了,这就是一个外部变量;
    如果SectionNumber的值等于0,Value成员的值不等于0的话,说明它是一个通用的符号,这个Value就代表符号占用的内存大小。

在这里插入图片描述

在这里插入图片描述

这个新增的1.c文件和我们Source.c文件,合并起来形成可执行程序,而我们现在Source.c里面主要看1.obj这个COFF文件,在代码里我们把1.obj文件里所有的节名都打印出来了:

在这里插入图片描述

然后我们又把所有的符号名字(标准符号元素和辅助符号元素)、辅助符号元素的个数(由于符号表是一个记录数组,每条记录长18字节。每个记录都是标准的或辅助的符号表记录。所以这个NumberOfAuxSymbols也是整个符号表数组的元素个数),都打印了出来:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们可以看到在1.c中引用的外部变量yyy,在符号表中就有了,它是第62个符号(符号表中的索引),如下图所示SectionNumber为0,Value也为0,说明这个符号是一个外部引用变量。

在这里插入图片描述

  • Type是说我们这个变量是什么样类型的变量,我们这个Type占2个字节,它由两部分组成,MSB最高有效位部分是指这个符号是否是复合类型(指针、函数、数组)(具体含义还要看下面的Base type的Description),还是说它不是复合类型none:

在这里插入图片描述

在这里插入图片描述

LSB最低有效位部分是指变量的基本类型,例如IMAGE_SYM_TYPE_CHAR、IMAGE_SYM_TYPE_INT、IMAGE_SYM_TYPE_DOUBLE、IMAGE_SYM_TYPE_BYTE或IMAGE_SYM_TYPE_DWORD等等;
这两部分结合起来也就能把我们这个变量的类型说清楚。
注意,LSB和MSB并不是指低位字节和高位字节,而是指有效位!例如Type=0x0020的话,它的LSB为0,MSB为2(因为1个十六进制数占4bit,而2的4次方=16,正好为基本类型的个数,所以1个十六进制数就足以表示变量类型了).
尽管 Microsoft 工具通常不使用此字段并将 LSB 设置为0,但是为基类型定义了以下值。相反,VisualC + + 调试信息用于指示类型。

  • Storage Class在.obj或.exe中常见的是IMAGE_SYM_CLASS_EXTERNAL和IMAGE_SYM_CLASS_STATIC这两种类型,其他的平常没有见过,例如上面的代码(第62个符号yyy),对于函数名或变量来说,(StorageClass=2,即IMAGE_SYM_CLASS_EXTERNAL:Microsoft 工具用于外部符号的值。在StorageClass为IMAGE_SYM_CLASS_EXTERNAL的情况下,如果节号SectionNumber是 IMAGE _ SYM _ UNDEFINED (0)的话,表示尚未为符号记录分配节,则Value表示该节的大小,此时Value的值也为零,因为没有为该符号分配节,说明这个符号是一个外部引用变量;
    如果节号SectionNumber不为零,则Value字段用来表示这个变量在它那个节里面的一个偏移量,相对于这个节开头的偏移)
    如果Storage Class的值为3(即IMAGE_SYM_CLASS_STATIC)的话,Value的值就为节里面的偏移量;如果Value的值为0的话,这个符号就表示一个节名。

在这里插入图片描述

在这里插入图片描述

上图可以看到.debug是一个节,它有两个辅助符号元素(i=6和i=7),而且通过观察我们发现,只要是节名后面都跟有2个辅助元素。
这个NumberOfAuxSymbols是指在这个标准元素后面跟了几个辅助符号元素(auxiliary symbol table entrie),有时候一个标准元素不足以说明符号的信息,那么就会在后面跟一个或几个辅助符号元素(Auxiliary Symbol Records ),辅助符号元素会增加符号的信息。
辅助符号元素的个数不会小于2,因为会把最后一个辅助符号元素置成0作为结束标识。

我们修改一下代码,打印一下Storage Class来看看:

在这里插入图片描述

在这里插入图片描述

我们可以看到大部分都是2和3,上图也出现了6,Storage Class为IMAGE_SYM_CLASS_LABEL,它是指源代码被编译器扩展为汇编代码时自动生成的那些标号。

IMAGE_SYM_CLASS_WEAK_EXTERNAL是指弱引用符号,什么是弱引用呢,例如下图所选择的代码,都没有赋值,都是弱引用:

在这里插入图片描述

int yyy=200; 赋值了,就是强引用;
在1.c源文件中又定义了一次pFile_Head和yyy变量:

在这里插入图片描述

链接的时候会报错,提示yyy变量定义了多次;给一个变量赋值的话就是强引用,没有赋值就是弱引用;
像上图代码中两个源文件都定义了同一个类型和名字的弱引用pFile_Head,编译链接的时候就会把这两个定义在不同文件中同名称的弱引用变量合并到一起,所以多次定义弱引用变量不会报错。
其实你可以在不同的源文件里面定义这种同名的弱变量,而且这种同名的弱变量的类型还可以不同,比方说你在1.c中定义了一个int y6=0;的变量,也可以再在Source.c里面定义一个char y6;,这两个同名但类型不同的变量在我们链接的时候并不冲突,并不会产生错误,我们说这种就是弱变量;

Auxiliary Symbol Records

Auxiliary Format 5: Section Definitions

我们先只看下图这种节名后面跟着的辅助节定义(该辅助符号记录跟标准符号记录一样占18个字节,但是它的成员变量变了,也就是它的数据所代表的含义变了)。

在这里插入图片描述

在这里插入图片描述

我们看上图的第8个符号.data(i = 8 kkkk 2 3 .data)(2是指该符号有几个辅助符号NumberOfAuxSymbols,3是指StorageClass=IMAGE_SYM_CLASS_STATIC),查看监视窗口中的pSymbol[8]中的值:

在这里插入图片描述

可以看到SectionNumber的值为3,指的是第2个节表(2 = .data)。
第9个符号是辅助符号(辅助节?),我们如下修改代码,把辅助符号pAux_Symbol给它重新赋值一下,把第9个辅助符号给搞出来,以便我们调试观察该辅助符号中的具体值:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们可以看到Length的值是4,它指节数据的大小,它的值和节表中的SizeOfRawData相同,因为第2个节pSction_Head[2]的数据大小SizeOfRawData也是4。

Number和Selection这两个成员只对COMDAT这种特殊的节有用。
Number是所关联节的节表中的基于1的索引。当COMDAT Selection设置为5时使用Number。
COMDAT Selection是个编号。如果该节是 COMDAT 节,则此Selection编号有意义。

字符串和重定位

COMDAT节

有这么一个COMDAT数据节,这个数据节里面的数据也就是我们C语言里面所定义的全局变量,这个数据节有什么特点呢?

The Selection field of the section definition auxiliary format is applicable if the section is a COMDAT section.
如果节是 COMDAT 节(IMAGE_FILE_HEADER.Characteristics属性包含0x1000),则节的定义辅助格式的 Selection 字段有意义。
COMDAT 节是可由多个obj文件定义的节。(标志 IMAGE _ SCN _ LNK _ COMDAT 在节头的Characteristics字段中设置。)
辅助格式中的Selection 字段确定链接器解析 COMDAT 节的多个定义的方式(例如Selection 的值为IMAGE_COMDAT_SELECT_ANY或IMAGE_COMDAT_SELECT_ASSOCIATIVE)。

-IMAGE_COMDAT_SELECT_ANY
定义了相同 COMDAT 符号的任何节可以被链接; 其余部分将被删除。

  • IMAGE_COMDAT_SELECT_ASSOCIATIVE
    如果链接了某个其他 COMDAT 节,则链接该节(The section is linked if a certain other COMDAT section is linked. )。
    另一个节由节定义的辅助符号记录的 Number 字段指示。此设置对于在多个节中具有组件(例如,一个节中的代码和另一个节中的数据)但必须将所有组件作为一个集合链接(也就是合并到一个exe文件)或丢弃的定义非常有用。与此节关联的另一个节必须是 COMDAT 节,它可以是另一个关联的 COMDAT 节。关联的 COMDAT 节的节关联链不能形成一个循环。节关联链最终必须到达一个没有 IMAGE _ COMDAT _ SELECT _ ASSOCIATIVE 集的 COMDAT 节。

The first symbol that has the section value of the COMDAT section must be the section symbol.
具有 COMDAT 节值的第一个符号必须是节符号(在标准符号格式中的Name字段,例如.data)。
该符号(标准符号格式IMAGE_SYMBAL)包含节的名称Name、Value字段等于零、所讨论的 COMDAT 节的节号SectionNumber、等于 IMAGE _ SYM _ TYPE _ NULL 的 Type 字段(没有类型信息或未知的基类型。 Microsoft 工具使用此设置)、等于 IMAGE _ SYM _ CLASS _ STATIC 的 StorageClass 字段以及一个辅助记录。
第二个符号称为“ COMDAT 符号”,由链接器根据 Selection 字段进行解析(辅助格式中的Selection 字段确定链接器解析 COMDAT 节的多个定义的方式)。

也就是说我们有两个同名的变量,例如1.obj和2.obj里面定义了两个同名的已赋值的变量,我们说这是强引用,按理说LINKER会报错,但是呢,如果你把这两个变量定义成__declspec(selectany) int x5 =100; 这种的话,说明可以同名(这时候x5所在节具有COMDAT数据),告诉链接器如果你碰到两个这种同名的变量,你选一个就可以了,不用报错,就是这个意思。
我们运行程序来看看:

在这里插入图片描述

在这里插入图片描述

这个x5是第12个符号,而它的SectionNumber是3,那么它就是在第2个节里:

在这里插入图片描述

我们看到第2个节.data的IMAGE_SECTION_HEADER.Characteristics的值中有0x1000这个值,它是IMAGE_SCN_LNK_COMDAT属性,说明这个节包含COMDAT数据:

在这里插入图片描述

我们说每个节名也是符号,我们再找这个节名符号.data:

在这里插入图片描述

我们看到第9个和第13个符号都是这个.data节名(在节表中是有两个.data节的),这两节名是一样的,所以我们x5有可能在第9个节名符号所代表的节里面,也有可能在第13个节名符号所代表的节里面,而且这两个节名后面都紧跟有两个辅助节(只需看第一个辅助符号,最后一个辅助符号是结尾符):

在这里插入图片描述

我们先看第9个符号,它的SectionNumber是3,也是第2个节,仍然是.data节,由于该节的Characteristics是IMAGE_SCN_LNK_COMDAT属性,所以我们就可以看第10个辅助符号:

在这里插入图片描述

如果这个辅助符号是对我们节定义进行说明的话,那么辅助符号的格式就是下图这种类型记录的格式:

在这里插入图片描述

一旦这个节是COMDAT节的话(第2个节.data的Characteristics的值中有0x1000这个值,它是IMAGE_SCN_LNK_COMDAT属性,说明这个节包含COMDAT数据),这个Selection就有意义了,否则的话Selection没有意义;
这个Selection就代表当你碰到两个同名变量的话,怎么把它俩合并;
刚才我们看到Selection的值为2,它就代表任何一个节如果有相同符号的话,能够进行链接,只链接一个符号,剩下的相同符号就会扔掉(the rest are removed);
例如Source.obj和1.obj这两个obj里面都有x5这个变量,那么它只会使用其中一个变量,所以链接的时候就不会报错。

在这里插入图片描述

这个COMDAT节只能出现在我们COFF格式文件里面,在IMAGE(即EXE)文件里面就不存在这个节了,因为EXE文件已经链接好了,这个节就没有存在的意义了。

我们再来看看正常的x6变量:

在这里插入图片描述

在这里插入图片描述

它是第16个符号,它的SectionNumber是第4个,也就是第3个节.data节(如下图所示),而且我们看到它的Characteristics里面没有0x1000这个代表COMDAT数据的值,说明它这个节里面都是正常的数据:

在这里插入图片描述

我们再看这个节名.data后面的辅助符号,也就是第14个符号:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们可以看到,这个里面Selection就没有了(Selection字段没有意义),这个里面就没有我们Selection的说明了;
所以说正常变量所在的节就没有COMDAT标识,这个时候如果两个.obj文件里面出现两个该变量标识的话就会报错了。

我们看到的标准符号格式是下图这种格式:

在这里插入图片描述

还有对标准符号进行说明的辅助符号(下图为Auxiliary Format 5: Section Definitions):
此格式遵循定义节的符号表记录。这样的记录具有一个符号名称,该符号名称是一个节的名称(如.text或者.drectve) ,并且具有存储类 STATIC (3)。辅助记录提供有关它所引用的节的信息。因此,它复制了节表中的一些信息。

在这里插入图片描述

String

String就是一堆字符串,而COFF字符串表中是一堆字符串,该字符串表的前4个字节代表它占了多大的地方(所有字符串所占的空间大小)(这个大小包括 size 字段本身,因此如果不存在字符串,则此位置中的值为4)。
字符串表前4个字节之后,就是由 COFF 符号表中的符号指向的以空结尾的字符串。

COFF String Table

紧跟在 COFF 符号表之后的是 COFF 字符串表。该表的位置是通过取 COFF 头中的符号表地址并加上将符号数乘以符号的大小来确定的,我们知道标准符号元素和辅助符号元素的大小都是18个字节:

在这里插入图片描述

上图中pSymbol这个指针变量所指向的类型IMAGE_SYMBOL就是我们标准符号的格式,所以根据C语言里面的指针特性,pSymbol+1就代表跳过一个符号元素,加2就代表跳过两个符号元素,所以我们加符号元素的个数pFile->NumberOfSections,就相当于我们找着紧跟在COFF符号表之后的字符串表的起始地址。
我们把这个字符串表起始地址添加到内存窗口中进行观察:

在这里插入图片描述

在这里插入图片描述

我们看到前4个字节的值为0x0000029d,后面的都是以0结尾的字符串(上图右栏中字符串后面的那个小黑点.),所有字符串所占的字节数是0x29d(十进制为669),我们用该字符串表开始地址加上29d的值为0x154F14F0DEC(就指向上图右栏中的C_Shutdown.rtc$TMZ的MZ位置那里),即我们整个obj文件里面的字符串。
那么字符串什么时候用呢?
我们再看一下标准符号的格式:

在这里插入图片描述

它的第一个成员是8个字节的名字,但是我们看到程序的输出,有的符号名字特别长,8个字节肯定放不下,例如我们来看看下图中的第22个符号:

在这里插入图片描述

在这里插入图片描述

此时ShortName肯定就不行了,我们前面讲过长名字LongName的低4字节如果是0的话(pSymbol[i].N.Name.Short的值为0),我们说它是一个长名字,高4字节是0x22(pSymbol[i].N.Name.Long的值),就表示我们从那个字符串表开始地址偏移0x22个字节,就是我们要找的这个名字。
Symbol Name Representation

在这里插入图片描述

如上图代码所示,如果Short等于0的话,就通过pString+pSymbol[i].N.Name.Long打印出这个长名字;如果Shot不等于0的话,我们就打印出它的短名字;
这就是符号它的名字的一个表示方法。

使用dumpbin输出符号内容

在这里插入图片描述

dumpbin不仅能把obj中的符号搞出来,它还能把辅助节里面的内容(Length、NumberOfRelocations、NumberOfLinenumbers、CheckSum)也给你搞出来:

在这里插入图片描述

dumpbin分析出来的结果应该和我们写的程序分析出的结果是一样的,我们可以对照着看一下。

重定位

COFF Relocations (Object Only)

在这里插入图片描述

上图这是随便一个节头(节表)的节名,在每个节里面有一个重定位指针(PointerToRelocation),这个重定位指针就指向这个节里面有些数据的重定位信息:

在这里插入图片描述

那什么时候需要重定位信息呢?

在这里插入图片描述

我们知道Add函数中的局部变量(例如变量c),这个局部变量没必要出现在符号里面,因为它是程序运行的时候才产生的。

在这里插入图片描述

yyy这个变量是第62个符号(符号表中的索引),如上图所示SectionNumber为0,Value也为0,说明这个符号是一个外部引用变量(StorageClass=2,即IMAGE_SYM_CLASS_EXTERNAL:Microsoft 工具用于外部符号的值。在StorageClass为IMAGE_SYM_CLASS_EXTERNAL的情况下,如果节号SectionNumber是 IMAGE _ SYM _ UNDEFINED (0)的话,表示尚未为符号记录分配节,则Value表示该节的大小,此时Value的值也为零,因为没有为该符号分配节,说明这个符号是一个外部引用变量;
如果节号SectionNumber不为零,则Value字段用来表示这个变量在它那个节里面的一个偏移量,相对于这个节开头的偏移)
(Value字段:与符号关联的值。此字段的解释取决于 SectionNumber 和 StorageClass。一个典型的意思是可重定位的地址,例如指定节内的偏移量)。
yyy这个变量是需要重定位的,aaa.b这个变量也需要重定位的,因为在我们1.obj里面yyy的地址我们是不知道的(外部引用的变量,该符号记录中的SectionNumber=0,即IMAGE_SYM_UNDEFINED,表示尚未为符号记录分配节;Value为零表示这个符号是对在别处定义的外部符号的引用)(说明这个符号是一个外部引用变量),所以说在链接的时候它要确定yyy的地址,而aaa的地址我们知道,它是在.data节里面的(.data节的地址我们知道);
但是我们链接器在把我们这个1.obj文件和Source.obj这俩进行链接的时候,它会把.data节进行合并,那么在合并后1.obj里面的.data节的位置就没法确定了,不确定的话`aaa.b = 200;这个地方引用的地址是不是就需要根据aaa这个变量在我们exe文件里面的位置,来把这个地址给它修正正确,那么怎么修正呢?我们的修正信息在哪呢?就在节头(节表)的PointerToRelocations指针所指向的重定位表里面(在obj文件中使用)。
也就是说,要把Add这个函数所生成的机器代码里面的表示yyy和aaa的地址进行修正。

我们看到第0个节的PointerToRelocations指针的值是0,而且NumberOfRelocations的值也是0,所以第0个节里面是没有重定位地址的。
我们看一下Add这个符号在哪个节里面。
注意,yyy这个符号所在的节不需要重定位,因为它这个里面的地址不需要修改,需要修改的地址都在代码里面呢哪段可执行代码引用了这个变量,那么这个变量的地址如果变化的话,这个可执行代码里面的地址需要修正,所以我们必须要找的是Add

在这里插入图片描述

在这里插入图片描述

Add是第60个符号,我们可以看到它的节号SectionNumber是第6个,也就是我们节表中的第5个节:

在这里插入图片描述

我们就可以看到它的重定位信息所在的位置了,并且第5个节里面有4个重定位信息(NumberOfRelocations),修改代码以便调试观察重定位信息都有哪些内容:

在这里插入图片描述

我们先来看一下重定位信息的格式:

在这里插入图片描述

  • VirtualAddress
    这个的意思就是,比方说有一段代码,其中使用了一个变量(例如yyy)的地址,那么这个VirtualAddress就是代码所在节的开始地址到该变量地址的偏移。
    VirtualAddress就是要对其应用重定位的项的地址。
    这是从代码所在节开始的偏移量,加上节表(节头)中的 RVA/Offset 字段的值。例如,如果节的第一个字节的地址为0x10,那么第三个字节的地址为0x12(重定位的项在该节中的偏移量加上引用该项的代码节的RVA)。

  • SymbolTableIndex
    我们这个重定位信息要对什么进行重定位呢,肯定是要对一个符号进行重定位,那么这个就是指向前面我们讲的符号表数组的那个索引,我们将要对这个索引指向的那个符号进行重定位。

  • Type
    这个是重定位的一个类型,你怎么重定位,重定位有好多种。

在这里插入图片描述

我们在这看到它有4个重定位信息(最后一个重定位信息pRelocation[4]应该是结尾标识,没有什么意思),我们看到Add函数中有yyy和aaa.b这两个需要重定位的变量,重定位的信息不止这两个,例如return a + b; 这句代码会添进去很多代码,里面也有需要重定位的,所以说重定位的信息不止我们这两个,它还有其他的,所以出来这4个,但是我们不知道这4个哪个是yyy的重定位信息、哪个是aaa重定位信息。
那么我们怎么看呢?

在这里插入图片描述

我们看到多了一个成员RelocCount,这个不算、不用管它,它和VirtualAddress组成一个union,所以这俩的值是一样的。

在这里插入图片描述

pRelocation[0]的SymbolTableIndex值是0x29,说明是第41个符号,这显然是系统里面的一个需要重定位的符号;
我们再看下一个重定位信息:

在这里插入图片描述

在这里插入图片描述

pRelocation[1]的SymbolTableIndex值是0x40,也就是第64个符号,这个也是系统里面的一个需要重定位的符号,这好像是一个函数。

在这里插入图片描述

在这里插入图片描述

pRelocation[2]的SymbolTableIndex的值是0x54,也就是第84个符号,这个就是yyy的重定位信息(pRelocation[2)。

在这里插入图片描述

而SymbolTableIndex=0x11,即第17个符号,就是aaa的重定位信息(pRelocation[3])。
这时候我们就更清楚了,除了yyy和aaa这两个变量需要重定位,多出来的两个是系统生成的需要重定位的符号。

使用工具IDA查看obj文件

我们用IDA这个工具来看我们的obj文件,它能反汇编我们的obj文件。

在这里插入图片描述

我们看到在上图左边的IDA函数窗口中没有yyy这个变量,因为yyy这是一个变量,IDA的函数窗口只显示函数的信息;
记住,yyy本身就不在1.obj文件里面,它在Source.obj里面(但是yyy这个外部符号在1.obj中的Add函数中被使用了)。

在这里插入图片描述

我们先看yyy的重定位信息(pRelocation[2]),它的偏移量(VirtualAddress)是0x41:

在这里插入图片描述

哎,这怎么是push rdi呢?哦,这个0x41是从我们这个yyy这个变量所在的这个节开始的偏移,那么它在哪个节上呢?

在这里插入图片描述

在这里插入图片描述

Add这个符号是第60个符号(因为yyy和aaa.b这两个变量是在Add函数中引用的,所以要查看Add这个符号),它的SectionNumber是6,即在第5个节里面。

在这里插入图片描述

而第5个节.text在文件里面的位置FOA是0x3d45,按照前面所讲的重定位概念,该节.text的FOA+重定位项yyy的偏移量=0x3d45+0x41=0x3d86,在IDA中查看这个地址明显不对啊!

在这里插入图片描述

我们看到IDA中Add的起始位置是0x38,变量yyy所在节的偏移量VirtualAddress是0x41,那么0x38+0x41=0x79,就是上图右边所选的这一句里面需要重定位(可以看到反汇编代码中的符号yyy),也就是从第0x79个字节开始我们需要重定位。

在这里插入图片描述

也就是代码里面给yyy赋值2的这一句代码产生的汇编。
在IDA中把机器码显示出来,可以看到所选中的4个字节(EF 00 00 00)就是需要对yyy进行重定位的地址。

在这里插入图片描述

那么这个EF是什么意思呢?
现在不确定这个EF是什么东西,EF 00 00 00这个地址要在我们链接的时候,链接器它能够找着这个yyy变量的真正地址,它能够把这个真正的地址填到这个地方。
yyy这个变量是第62个符号,它的符号记录中的SectionNumber为0,即IMAGE_SYM_UNDEFINED,表示尚未为符号记录分配节;Value为零表示这个符号是对在别处定义的外部符号的引用(说明这个符号是一个外部引用变量)。Value为非零值说明它是一个通用符号,其大小由Value指定。
(StorageClass=2,即IMAGE_SYM_CLASS_EXTERNAL:Microsoft 工具用于外部符号的值。在StorageClass为IMAGE_SYM_CLASS_EXTERNAL的情况下,如果节号SectionNumber是 IMAGE _ SYM _ UNDEFINED (0)的话,表示尚未为符号记录分配节,则Value表示该节的大小,此时Value的值也为零,因为没有为该符号分配节,说明这个符号是一个外部引用变量;
如果节号SectionNumber不为零,则Value字段用来表示这个变量在它那个节里面的一个偏移量,相对于这个节开头的偏移)
(Value字段:与符号关联的值。此字段的解释取决于 SectionNumber 和 StorageClass。一个典型的意思是可重定位的地址,例如指定节内的偏移量)。

在这里插入图片描述

我们看到第4个重定位信息(变量aaa所在节的重定位信息pRelocation[3])的VirtualAddress的值是0x4b,Add函数的开始地址是0x38,那么0x4b+0x38=0x83,也就是下图红线所划的地址89 FF FF FF:

在这里插入图片描述

这个就是aaa变量需要重定位的地址,这个89 FF FF FF是现在这个里面的地址,那么这是个什么地址呢?
64位汇编里面所有的地址都是相对地址,64位汇编里面没有绝对地址,这个是跟32位汇编不一样的地方。
我们看到0x81这句指令下面的指令地址是0x8B,我们把0x8B加上FFFFFF89就是0x14,为什么是0x14呢?
我们知道,yyy是外部引用变量,不是在1.obj里面定义的,所以引用yyy的那里的相对偏移地址没用处,在链接的时候会被替换成正确的地址;
而aaa是在1.obj里面定义的,所以

在这里插入图片描述

我们看到aaa这个变量的地址是0x10,而aaa这个结构体的定义如下:

在这里插入图片描述

我们看到AAA这个结构体中的b成员相对于AAA开始地址还要加一个4才是b成员的地址,Add函数里面我们是对aaa.b进行赋值的,所以0x10还要加上一个4才是aaa.b的地址,所以这里算出来是0x14。

在这里插入图片描述

我们64位汇编里面像上图所选的地址都是相对地址,这个相对地址就是你要引用的aaa.b这个变量的地址,这个地址是怎么获得的呢?
0x14-0x8B=0xFFFFFF89

在这里插入图片描述

下一条语句的地址8B加上89 FF FF FF这一个相对的偏移量,它才能够得到aaa.b的地址(0x14),64位程序里面都是相对地址,都是这么算的。
64位的相对地址能够减少重定位的工作,相对偏移量能够减少程序装入内存的时候要对地址进行修正的这么一个过程。

如何进行修正呢?
当我们的obj文件链接成exe文件的时候,它这个节会重新给你布置,会导致相对偏移量就不对了,所以要根据obj合并成exe以后的新的布局来调整我们这个相对偏移量。
我们可以看一下调整以后长什么样:

在这里插入图片描述

上图所选中的就是调整以后的相对地址,它就是根据刚才所说的obj文件里面节的位置变化以后,这个相对量是链接器链接的时候调整的相对地址,调整的依据就是pReloction[2]和pReloction[3]这俩重定位信息:调整哪个位置、怎么调整都在这个重定位信息里面。

我们再来看看Sub这个函数符号所在的节:

在这里插入图片描述

在这里插入图片描述

从上图我们可以看到Add在第5个节(SectionNumber=6),Sub是在第7个节(SectionNumber=8),从下图可以看到这两个函数都在.text$mn节中,但是这两个符号开始的位置是不一样的:

在这里插入图片描述

在这里插入图片描述

可以从IDA看到这两个符号的起始位置是不一样的。

重定位的类型

重定位记录的 Type 字段指示应该执行哪种重定位。为每种类型的计算机定义了不同的重定位类型。
Type Indicators

在这里插入图片描述

在这里插入图片描述

例如上图中的IMAGE_REL_AMD64_REL32类型(0x0004,The 32-bit relative address from byte following the relocation 从重定位后的字节开始的32位相对地址,即需要重定位的4个字节后面的下一个字节开始的32位相对地址)
这个类型是什么意思呢?就是如下图所示,相对于选中的4个字节机器码所在位置的下一个字节开始的地址(如下图所示的0000000000000099)的32位相对地址,即这4个字节是相对于0000000000000099这个地址的一个32位的偏移量(相对地址)
所以说,VirtualAddress字段指明了我们要对哪个地方开始的字节进行修正,SymbolTableIndex字段指明了我要修正的是哪个符号,Type字段指明了我怎么对这4个字节进行修正。

在这里插入图片描述

我们来看看yyy使用的类型,和aaa使用的类型,它们的类型是一样的:

在这里插入图片描述

这个8是IMAGE_REL_AMD64_REL32_4类型(0x0008,The 32-bit address relative to byte distance 4 from the relocation 相对于重定位的字节距离4的32位地址、从重定位到字节距离4的32位相对地址、从重定位开始的占4字节的32位相对地址)

在这里插入图片描述

在这里插入图片描述

我们可以看到确实是32位4个字节的相对地址(The 32-bit address relative to byte distance 4 from the relocation 相对于重定位的字节距离4的32位地址、从重定位到字节距离4的32位相对地址、从重定位开始的占4字节的32位相对地址)。
而且,从上图可以看到,yyy和aaa.b在反汇编代码中的相对地址所在地址的差值:0x83-0x79=0xa,而这两个变量所关联的重定位记录中的VirtualAddress的差值:0x4b-0x41=0xa,所以The 32-bit address relative to byte distance 4 from the relocation 这句英文的意思我感觉是,从重定位记录开始地址的。

重定位记录中的VirtualAddress就是要对其应用重定位的项的地址。
这是从代码所在节开始的偏移量,加上节表(节头)中的 RVA/Offset 字段的值。例如,如果节的第一个字节的地址为0x10,那么第三个字节的地址为0x12(重定位的项在该节中的偏移量加上引用该项的代码节的RVA)。
这个的意思就是,比方说有一段代码,其中使用了一个变量(例如yyy)的地址,那么这个VirtualAddress就是代码所在节的开始地址到该变量地址的偏移。

注意,这个类型跟处理器的类型相关,根据不同的处理器,它这个类型还是有区别的,各种处理器类型的重定位方式是不一样的。

每个obj里面每个节它都有自己本节的重定位信息;
每个节里面的重定位信息在exe文件里面是不存在的,后面讲PE的时候再说,现在我们讲的是COFF里面的重定位信息。

Grouped Sections (Object Only)

Grouped Sections

我们再看一个节的名字的时候,我们会看到这个$美元符号,这个美元符号是干啥的呢?

在这里插入图片描述

假设有.text A 和 . t e x t A和.text A.textB两个节,链接器碰到这两个节,它要把这两个节进行合并,为什么要合并呢?
链接器认为美元符号后面的字符、包括美元符号,都不是我们真正的节名,当我们把这个节合并到exe里面的时候,真正要使用的节名是 符前面的名字。那么 符前面的名字。 那么 符前面的名字。那么符后面跟的字符是干啥用的呢?
因为在obj文件里面.text$A这是单独的一个节,.text$B也是单独的一个节,链接器会把这两个节合并成一个节,但是合并成一个节的时候会有一个问题,到底是.text B 这个节位于 . t e x t B这个节位于.text B这个节位于.textA这个节前面,还是.text A 这个节位于 . t e x t A这个节位于.text A这个节位于.textB这个节前面呢?
这俩节谁在前、谁在后呢,它有一个规则,链接器会看 符后面的字符,按照 ∗ ∗ 字母顺序 ∗ ∗ 进行排序,合并成一个节。所以 符后面的字符,按照**字母顺序**进行排序,合并成一个节。 所以 符后面的字符,按照字母顺序进行排序,合并成一个节。所以这个美元符号是合并节来用的,Grouped Sections说的就是这个意思。

COFF里面还有一些节的内容,我们统一放在PE文件里面来讲。


COFF文件概览

本节讲一下COFF的全貌,可以对COFF有更进一步的认识,还有一些细节上的东西再说一下。

COFF文件=文件头+代码(Add/Sub)+数据+调试信息+重定位信息+符号+字符串
IMAGE_FILE_HEADER+.text+.data+.debug

在这里插入图片描述

obj文件中的节

我们先看一下上图代码中都有哪些节,每个节里面都有什么内容,我们以实际的obj文件为例子说明一些细节。

在这里插入图片描述

.drectve节

里面的内容是一些链接命令,实际上是一堆字符串,这堆字符串是一些链接命令。

在这里插入图片描述

点击上图选中位置右边那个放大镜图标,会打开一个显示文本信息的列表框:

在这里插入图片描述

/DEFAULTLIB链接命令的意思是,在链接的时候要包含的lib库文件;
/EDITANDCONTINUE链接命令就是让我们在调试的时候,可以修改在断点处下面的那些代码,改完之后还可以让你继续执行,它就会按照改完以后的新编译好的来执行。
/alternatename就是替补命令,用__CheckForDebuggerJustMyCode来替代__JustMyCode_Default @这个不好使用的名称,相当于起了一个别名。

所以,.drectve这个节也是一些辅助信息,辅助我们链接器把我们这个1.obj文件很好的和Source.obj链接成一个exe文件。

.bss节

.bss节里面放的是我们未初始化的变量(数据),.bss里面放的未初始化的变量是你初始化成0的那些变量,例如下图中的变量p1和y7:

在这里插入图片描述

那像1.c中的int y6;这种没有赋值的变量,我们说这种变量它是弱变量,你可以在不同的源文件里面定义这种同名的弱变量,而且这种同名的弱变量的类型还可以不同,比方说你可以再在Source.c里面定义一个char y6;,这两个同名变量在我们链接的时候并不冲突,并不会产生错误,我们说这种就是弱变量;
这种变量就比较特殊了,这种变量虽然没有初始化,但是这种变量它不会放在我们.bss节里面,.bss节里面放的是我们初始化成0的变量;

.data节

在这里插入图片描述

上图3、4这两个.data节就放的是我们初始化好的变量,比方说int x6=200;这个就是初始化好的变量就放在.data节里面了。

在这里插入图片描述

我们看到变量p1就放到第2个节里面去了(SectionNumber=3),p1初始化成NULL了(int* p1 = NULL),也就是p1初始化成0了,所以它就放在.bss节里面去了。

在这里插入图片描述

我们看到y6的SectionNumber=0,表明y6这个变量不在任何一个节里面,它哪个节都不是,它的StorageClass=2,即IMAGE_SYM_CLASS_EXTERNAL,说明y6是一个外部变量,并且当它的SectionNumber=0的时候,Value用来表明该符号的大小;

在这里插入图片描述

y6是符号表中的第12个变量,调试观察可以看到Value=4,当StorageClass=2,并且当它的SectionNumber不为0的时候,Value用来表示这个变量在它那个节里面的一个偏移量,相对于这个节开头的偏移;
但是如上图所示,当SectionNumber=0的时候,Value用来表明该变量的大小(y6的定义是int类型)。

显然我们在1.c中定义这个y6变量的时候,我们认为它是我们这个1.obj里面的一个变量,但是在我们编译好的1.obj文件里面,它其实是作为一个外部变量存在的,也就是我们这个1.obj文件里面并没有给这个y6变量分配空间,它是作为一个外部变量存在的。
换句话说,这种方式就促成了我们前面讲过的弱变量这种特性,我们可以在Source.c里面也可以定义一个y6,如果这个项目当中所需要的obj文件比较多的话,比方说需要10个源代码,在这10个源文件里面可以定义10个y6(可以是不同类型,例如int、char、struct结构体、longlong这种64位的),那这10个y6都是外部变量,对于这10个源文件里面声明的y6来说都是一个外部变量,那么当我们链接器要把它们链接成一个exe文件的时候,我们链接器怎么处理呢?
我们链接器就可以看见10个外部变量,那这个时候我们链接器就会把这10个y6放在一起作为一个变量来使用,会把这些变量合并成一个y6,链接器会看这些y6里面哪个类型的长度最长,比方说struct结构体类型的y6占有32个字节的话,这个时候链接器就会选择这个最长字节的类型作为我们y6的长度,这样的话我们32个字节的一段空间,即可以当作int来使,也可以当作char、struct、longlong来使了,这样的话编译器就把它们合并到一起了,这就是我们所看到的弱变量的一种特性。

在这里插入图片描述

我们说y7是一个强变量,所以说它的SectionNumber是3,也就是第2个.bss节,因为y7初始化成0了(int y7 = 0;),初始化成0所以放在.bss节里面,.bss节里面放的都是初始化成0的变量。

在这里插入图片描述

那我们再看x6,它的SectionNumber=5,所以它放在第4个.data节里了:

在这里插入图片描述

在这里插入图片描述

而x5这种变量前面我们讲COMDAT节数据的时候已经讲的很详细了,这种变量就是说你对它进行了一个初始化,但是你又不想让它在编译期间产生这种同名变量的冲突,你可以给该变量添加selectany这么一个前缀,那么编译器就会选择该同名变量的其中的一个来作为一个基准,其他同名变量就扔掉了;
你定义了5个x5,编译器只会用一个x5,不会报错,这种变量在我们ATL里面也有用到。

.msvcjmc节

在这里插入图片描述

.msvcjmc节是干啥用的呢?你要看这个节是干啥用的,在微软官方网站的说明里面,它没有提这个节,那我们怎么看呢?我们只能找一些蛛丝马迹自己来看。
我们看到这个节的SectionNumber应该是6,所以我们就看SectionNumber是6的符号有哪些:

在这里插入图片描述

我们找着.msvcjmc这个节里面这么多的变量,这个节里的变量都在上图所选的地方,这些变量是干啥用的呢?
这些变量是用于调试的,我们这个1.obj文件是按照调试版本来编译的(Debug x64),编译出来的obj就会出来这些变量,这些变量是用来在调试检查代码的时候进行使用的。

我们可以看一个例子:

在这里插入图片描述

把上图选中的这个符号所在的内存地址放到rcx寄存器里面,作为第一个参数传递给我们__CheckForDebuggerJustMyCode这个函数里面,我们可以在call这一句代码打上一个断点,F5、F11进到这个调试检查的函数代码里面了。

在这里插入图片描述

上图选中的__B9B818ED_1@c这个变量就是下图选中的那个符号,它的SectionNumber=6,也就是说这个变量就在第5个.msvcjmc节里面。
所以说.msvcjmc节里面的变量都是我们调试的时候用的,因为我们是调试版本的obj,所以产生出来的这一对变量。

我们来对这个调试检查代码寻根问底,看看__CheckForDebuggerJustMyCode这个函数到底干了啥。

在这里插入图片描述

你读这个调试检查代码就会发现,无论如何这个函数到最后都会成功执行,为什么呢?
rcx是刚才那个调试产生的变量__B9B818ED_1@c的内存地址,把这个内存地址放在rsp+8里面,在上图所选的这句话执行的时候,我们rsp这个寄存器所指向的内存地址,里面保存的是我们这个函数的返回地址。

我们用静态分析工具IDA打开COFF格式的文件1.obj,双击__CheckForDebuggerJustMyCode看看这个函数的反汇编代码和VC给我们生成的反汇编代码有什么区别:

在这里插入图片描述

可以看到该函数里面有一个extrn关键字,说明这是一个外部函数,也就是说在我们这个1.obj里面没有这个函数的代码。
那么当我们1.obj里面没有这个__CheckForDebuggerJustMyCode函数的代码,当链接器链接的时候,把1.obj和Source.obj这俩链接成我们exe的时候,我们在VC里面反汇编所看到的__CheckForDebuggerJustMyCode这个函数的反汇编代码它是从哪来的呢?
显然这个函数的这段反汇编代码已经放到我们这个exe文件里面了,那这个函数的代码是从哪来的呢?

在这里插入图片描述

dec我们说是链接器命令行,也就是说/DEFAULTLIB、/EDITANDCONTINUE、/alternatename这几个命令放在我们1.obj里面,link再把我们1.obj和Source.obj两个链接成exe的时候,显然这两个obj里面的代码是不够的,因为我们还有一个__CheckForDebuggerJustMyCode函数的代码,显然这个代码是1.obj外部的一个代码,这个函数的代码没有在我们1.obj里面,它肯定也不在外面Source.obj里面,但是显然当我们链接exe的时候,这个函数的代码就出来了,它从哪出来的呢?
链接器看见你这些编译链接命令以后(/DEFAULTLIB等命令),它同时还会把uuid.lib、LIBCMTD、OLDNAMES这几个库也链接到这个exe里面。
lib是库,学c语言的时候老是都会告诉你这是一个静态库,这个里面有一些函数的代码。
我们刚才所说的那个__CheckForDebuggerJustMyCode函数代码应该在LIBCMTD这个库里面,把这个库里面的代码放到exe里面,这个函数的代码不就有了么。
所以说这就是链接命令的一个作用,也就是帮我们link链接器把obj链接成可执行文件exe的辅助的一些信息,没有这一堆链接命令的话,我们是没法找到__CheckForDebuggerJustMyCode函数的代码是在什么地方的。

在这里插入图片描述

你在IDA所打开的1.obj的反汇编代码里面看不见这个__CheckForDebuggerJustMyCode函数的代码,因为这个函数的代码没有在我们这个obj文件里面,所以IDA静态反汇编到这就没有办法了。

当然你可以用IDA静态反汇编一下编译链接生成的exe文件,那是可以的,但是我们现在用另外一个工具x64dbg把生成的这个exe可执行文件打开。

在这里插入图片描述

因为我们知道__CheckForDebuggerJustMyCode函数的代码在exe里面肯定有,所以我们可以在x64dbg的符号标签页那里点击该exe可执行文件模块,然后在右侧搜索编辑框那里输入__CheckForDebuggerJustMyCode函数名就能够找到:

在这里插入图片描述

双击搜索到的该函数名,进入该函数的反汇编代码,然后我们在该函数开头打一个断点,再按两次F9运行到函数这个地方:

在这里插入图片描述

现在我们就可以看反汇编出来的这个函数的代码,可以跟我们VC那里的反汇编代码一行一行的进行比较:
前两句代码一样,VC中的第三句代码里的JMC_flag和x64dbg中该位置的代码就不一样了,x64dbg中的是rsp+40,显然VC中的就没有在x64dbg中看的更清楚。
也就是说,rsp首先指向的是返回地址,然后rsp减了一个38h,之后又加上了40h,正好指向的是返回地址上面的参数1位置,也就是第三句代码执行后,我们参数1的值放在了rax寄存器里面了。
可以按F8单步执行验证一下:

在这里插入图片描述

我们可以看到执行第三句代码以后,rax=rcx=参数1的值,第一个参数的值就是变量__B9B818ED_1@c这个符号所在的内存地址;
也就是说在VC中的mov rax, qword ptr [JMC_flag]这个地方的JMC_flag我们看不明白这是什么,但是我们在x64dbg这个调试器里面就能够看明白了。

在这里插入图片描述

上图选中的这行代码,在这个地方我们要注意一下call qword ptr ds:[<&GetCurrentThreadId>]这句代码,获得当前线程ID,这个函数编程的时候是我们经常用的,我们用这个函数搞点小东西出来,我们把断点达到这句代码。

在这里插入图片描述

当执行上图cmp这句代码的时候,我们点击这一句就能从下面窗口那里看到dword ptr ds:[00007FF784E3A43C <project34.__DebuggerCurrentSteppingThreadId>]=0,然后我们双击这一句就能够在下方的数据窗口那里看到00007FF784E3A43C地址处的内容:

在这里插入图片描述

再回到反汇编代码那里,如果cmp这句代码的比较条件成立的话,那么下一行的je指令就能够执行跳转,会跳转到函数结尾:

在这里插入图片描述

那么我们如果想要call这句代码执行的话就必须要cmp这句代码里面的__DebuggerCurrentSteppingThreadId这个变量不等于0才行。
我们可以在数据窗口中修改这个变量的内容(鼠标右击、二进制编辑、编辑),随便改一个值(例如ASCII的值=1),然后再F7执行到je那句代码就不会跳转了,接着call就能够执行了。
按F7跟进这个call:
在这里插入图片描述

这个就是GetCurrentThreadId获得当前线程id的函数代码,这个时候我们就猜测,此时gs寄存器里面有可能放的就是线程的TEB。

在32位程序里面TEB是放在FS寄存器里面,但是64位的TEB是放在GS寄存器里面的。
我们把windbg打开,windows操作系统要在调试模式下(在CMD中执行bcdedit -debug on),并已加载下载好的微软的符号,执行dt _TEB命令:

在这里插入图片描述

这个TEB的结构就出来了,我们可以看到,gs寄存器的第30个字节是在TEB的NtTib里面,我们就执行dt _NT_TIB命令:

在这里插入图片描述

第30个字节是Self,也就是指向自己。

在这里插入图片描述

所以rax寄存器放的就是TEB的首地址,TEB的第48个字节在_CLIENT_ID这个结构体里面,输入dt _CLIENT_ID命令:

可以看到正好是两个变量,一个是我们进程的ID,另一个是我们线程的ID,所以它就是通过这种方法,直接从gs寄存器里面找到我们线程TEB块,在TEB块的第48个字节就是我们的线程ID。

Release版本obj

如果我们不是Debug x64编译的话,那么还会出来.msvcjmc节中的那一堆符号么?
我们编译一个Release版本的obj,看看这一对符号还出不出来(先在VC中选择Release编译模式编译一个obj出来,并修改CreateFile函数中的obj文件路径参数,然后再选择Debug编译模式,调试1.obj文件):

在这里插入图片描述

在这里插入图片描述

当调试单步执行到上图47行时,我们观察pFile_Head变量,发现里面的成员的值都不对,例如Machine为0,NumberOfSections为0xffff,哪有这么多的节,这明显就不对;这是怎么回事呢?
这时候就会怀疑是我们VC调试的问题,那我们用微软的工具dumpbin看一下1.obj文件:

在这里插入图片描述

我们发现微软给的这个工具也认不出来这个Release版的1.obj的格式了,说明不是COFF格式了,所以Release版本的obj文件,我们就不知道微软给的是什么格式了,这就搞不清楚了,那么我们怎么办呢?
我们用命令行的方式把1.c源文件编译成1.obj文件:

在这里插入图片描述

这个时候我们编译出来的就是Release版本的1.obj了,我们再修改Source.c源代码中的CreateFile函数的参数,打开命令行编译出来的1.obj文件:

在这里插入图片描述

在这里插入图片描述

你现在就看不见哪些乱七八糟的.msvcjmc节中的符号了吧,连.msvcjmc这个节都没有了,这才是Release版本的真正样子,调试检查的那一堆东西就没有了。

.text是代码节,这里面放着我们的可执行代码。

.xdata节和.pdata节简介

.xdata和.pdata这两个节是干啥用的,这两个节和函数的调用有关系,而函数的调用和栈的内存分配是紧密关联的,还有那个函数自己的“开场白”和“收场白”,这两个节还和结构化异常处理有关系;
也就是说,这两个节和函数的调用、栈的内存分配、函数的“开场白”和“收场白”、结构化异常处理都有关系,因为这几个东西是连在一起的,这里简单的记住这几个东西和这两个节有关系就行了。

函数的“开场白”和“收场白”

现在我们简单的说一下函数的“开场白”和“收场白”是什么。

函数的“开场白”

在这里插入图片描述

我们跳到Add这个函数里面,在这个函数里面我们可以看到,真正函数的代码是从mov dword ptr [c], 0C8h这一句开始的,而从mov rdi, rsp这句到call __CheckForDebuggerJustMyCode这一段代码,分别是对开辟的栈空间进行初始化和调用调试检查代码,为什么是用0xCCCCCCCCh初始化呢,因为0xCC是int 3指令的机器代码,代码执行到该指令就会中断下来。
函数开头的两行反汇编指令是将函数参数从rdx和rcx寄存器中放入栈内存中,然后将函数中要用到的rbp、rdi寄存器的值压入栈中保存起来以便恢复,因为这俩寄存器里面的值你不能破坏,你的函数退出的时候,你得把这俩寄存器里面的值恢复。

但是我这为什么不保存eax、ecx寄存器的值呢,而只单独保存rbp、rdi这俩寄存器的值呢?为什么eax和ecx寄存器里面的值改了就没有关系呢,这是为什么呢?
我们在写程序的时候,不但有硬件对我们的要求:
我们在学汇编语言的时候,哪个寄存器可以和哪个寄存器搭配,哪个寄存器不能和哪个寄存器搭配,哪个寄存器是干啥用的;你比方说我们这个rip这个指向下一条可执行指令的寄存器,这个寄存器的作用就是专用的,这就是CPU的硬件要求,你不按照这个硬件要求编程就不行。
同时我们在写程序的时候,不但有硬件要求,我们还有软件要求,这些软件要求是谁提出来的呢,谁给我们提的软件要求呢?
硬件的厂商只提硬件的要求,软件的要求是操作系统的厂商(比如微软)提出来的,我们把这个东西统一叫ABI(应用程序的二进制接口),我们现在所写的windows应用程序都符合windows应用程序二进制接口的规范,不仅windows应用程序,windows内核驱动程序也是一样的。
那么在Add函数里面我们只保存rbp和rdi这两个寄存器的值,而不管ecx和eax里面的值,虽然我们改变了ecx和eax,但是我们不用还原ecx和eax寄存器的值,为什么我们可以不还原呢,就是微软的软件要求里面告诉我们哪个寄存器你可以用了不管,哪个寄存器你用之前必须要把原来的值保存一下,这个就是软件要求里面会写明白。
当然,我们用的是微软的编译器和链接器,所以我们不用考虑这些东西,但是你如果要用汇编语言写代码的话,像这种ABI软件要求你就得了解了,如果你不知道这些微软的应用程序二进制接口,那么你用汇编语言写windows程序的时候,你会碰到很多问题的,所以这个就是我们不喜欢用汇编语言写windows程序的一个原因,因为这些规则了解起来太麻烦了,当然这些规则我们以后会专门讲的,毕竟我们的学习已经这么深入了,这里大家先了解认识一下就行。

函数的“开场白”,其实就是对我们一些寄存器值的保存,栈内存的分配,函数参数的设置,这些东西统统称为函数的“开场白”。

函数的“开场白”和“收场白”

在这里插入图片描述

上图中函数结尾选中的这几句就是我们函数的“收场白”,恢复寄存器,把函数开头分配的栈空间收回去。

.chks64节

在这里插入图片描述

这个.chks64是微软独有的一个节,我们没有查到这个节到底是干啥用的,这个节的SectionNumber是9,但是我们在符号表中没有看到有这个编号的符号:

在这里插入图片描述

而上图选中的只是节名,这个看不出啥东西来,由于没有一个符号在这个节里面,所以我们确实不知道这个节是干啥用的了。

修改节名

在这里插入图片描述

我们还可以给节命名,你比方说可以把.bss节中的y7变量,放到我们自己命名的.YouData节里面,我们看一下程序输出(记住修改CreateFile的1.obj路径参数为Debug版本):

在这里插入图片描述

在这里插入图片描述

这个.YouData节里面放的是y7这个变量,我们可以查一下y7这个变量:

在这里插入图片描述

我们可以看到y7的SectionNumber=4,所以确实是在第3个.YouData节里面,我们可以通过这种办法改这个节名。

在这里插入图片描述

如果你想把我们Add这个函数的代码放在.abc这个节里面的话,如下修改代码即可:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/zhaopeng01zp/article/details/131283557
PE
今日推荐