Bin

BIN文件是纯二进制文件,没有固定的结构或元数据。==当想要将二进制文件烧录进单片机时,不需要再对数据进行处理==,也就是说,我们使用ImHex打开的原始数据是什么样的,那么我们查看Flash中的内容就是什么样的。

Hex

Hex 全称 (Intel HEX)文件是由一行行符合Intel HEX文件格式的文本所构成的ASCII文本文件。

Intel HEX文件中,每一行包含一个HEX记录。

ASCII表示

[!note]

注意:我们使用txt文件打开的时候显示的是ASCII表示,也就是一个字符串。实际上一行数据的原始数据如下图左侧所示,使用txt打开的如右图所示。

Imhex打开

下面会用源数据表示左侧的数据,用ASCII表示右侧数据。

参考链接

单片机烧录用的hex文件,文件格式解析(转载)_.hex用什么软件打开-CSDN博客

Intel Hex Format

行结构

一行Hex数据由以下结构组成:

1
[Start code][Byte count][Address][Record type][Data][Checksum]
参数 长度 说明
Start code - 每行都由冒号开头,这是固定的
Byte count 1 字节 本行数据的长度,最大值是255 (0xFF). 16 (0x10)32 (0x20)是最常用的
Address 2字节 Memory数据开始16-bit地址偏移。物理地址通常是有这个偏移加上基地址
Record type 1 字节 表示不同数据段的含义
Data 可变长度 具体数据 N Byte ,表示本行中数据字节的数量
Checksum 1 字节 校验值

[!tip]

Checksum的计算方法为:

0300300002337A1E,数据和为:03 + 00 + 30 + 00 + 02 + 33 + 7A = E2 这个E2的补码是 1E ,即这个数据record的补码

[!note]

注意:上表的长度是ASCII的长度,也就是假设txt显示:1010,那么10就是一个字节,但是在源数据中,这就是两个字节0x31 0x30

==Record type的选取==

数值 数据类型详(Record type)
00 表示后面记录的是数据
01 表示文件结束,放在文件的最后,标识HEX文件的结尾
02 表示扩展段地址
03 表示开始段地址
04 表示扩展线性地址
05 表示开始线性地址

[!important]

这里需要注意的是Bin文件和Hex文件的区别:

  • Bin文件中的数据内容可以直接烧录进入Flash中

  • Hex文件中的数据内容(指的是源数据)是经过改变之后才可以放入Flash中的

    一般我们通过txt文件看到的都是ASCII表示,并不是文件源数据,我们需要将ASCII的数据当成16进制的数据,也就是我们看见的:1010转换为0x10 0x10。此时他们的源数据就改变了。源数据从0x3A 0x31 0x30 0x31 0x30变成0x10 0x10 0x10 0x10

地址解读

因为HEX的地址段只有两个字节表示,只能寻址到16位,显然是不够的,所以就有了拓展地址,相关解释请参考Intel Hex Format

地址分为16位和32位或8位。

  1. 8位和32位:==数据记录中的数据字节在内存中的绝对地址为线性地址==。
  2. 16位:==数据字节的绝对地址是分段地址==。

==拓展地址空间==
$$
实际物理地址=基地址+偏移地址
$$

地址类型 Record type 地址解读
线性基地址 0x04 得到的数据左移16位
段基地址 0x02 得到的数据左移4位
偏移地址 0x00 得到的数据

[!tip]

选择不同类型的基地址(0x020x04)对应着不同位长的系统。

==指定程序入口==

  1. 开始段入口:
    $$
    实际入口地址=(代码段地址<<4)+指令指针地址
    $$

    地址类型 Record type 地址解读
    代码段地址(CS) 0x03 得到的数据的高2字节
    指令指针地址(IP) 0x03 得到的数据的低2字节
    • 记录::0400000300003800C1
      • 数据字段:00003800
        • CS = 0x0000
        • IP = 0x3800
      • 实际入口地址 = (0x0000 << 4) + 0x3800 = 0x03800
  2. 开始线性入口:可以直接使用数据字段的值。

    • 记录::04000005000000CD2A
      • 数据字段:000000CD
      • 实际入口地址 = 0x000000CD

==示例==

以下是一个包含扩展地址记录的HEX文件示例:

复制

1
2
3
4
:020000021200EA
:10000000AABBCCDDEEFF00112233445566778899F1
:10001000ABCDEF0123456789ABCDEF0123456789B2
:00000001FF
  • 第一行(类型02):
    • 数据字段:1200(段地址)。
    • 基地址 = 0x1200 << 4 = 0x12000
  • 第二行(类型00):
    • 偏移地址:0000
    • 实际地址 = 0x12000 + 0x0000 = 0x12000
    • 数据字段:AABBCCDDEEFF00112233445566778899
  • 第三行(类型00):
    • 偏移地址:0010
    • 实际地址 = 0x12000 + 0x0010 = 0x12010
    • 数据字段:ABCDEF0123456789ABCDEF0123456789
  • 第四行(类型01):
    • 文件结束记录。

实现

以下使用CAPL语言实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
variables
{
struct Block {
dword BlockStartAddr; //数据开始的地址
dword BlockDataLength; //数据的长度
byte dataBuffer[0x020FFFF]; //数据区域(单块数据的Buffer,如果hex文件很大,则需要把次参数调大)
};
struct Block hexfile[5]; //创建5个数据块(主要根据HEX文件进行调整,hex有几个数据块就需要设置几个)
int HexBlockTotalNumber = 0; //数据块总数
dword t1; //计算解析所用的时间
}


/*********************************************************************************
*Function: //char2byte
* Description: //把单个字符转换为Byte的函数
*Input: //ch:ASCII编码字符,取值为0到F
*Return: //val,为byte类型
**********************************************************************************/
byte char2byte(char ch)
{
byte val;
val = 0;
if ( ch >= '0' && ch <= '9')
{
val = ch - '0';
}
if ( ch >= 'a' && ch <= 'f')
{
val = (ch - 'a') + 10;
}
if ( ch >= 'A' && ch <= 'F')
{
val = (ch - 'A') + 10;
}
return val;
}

/*********************************************************************************
*Function: //Read_hexFile
* Description: //解码HEX文件,只支持0x00,0x04,0x01类型
*Input: //Filename:需要解码的文件名
*Output: //hexfile
*Return: //void
**********************************************************************************/
//读取HEXFILE
void Read_hexFile(char Filename[])
{

long file_handle;
char RowData[128]; //逐行读取,每行数据缓存,当每行数据大于128时,需要将其调整
dword i;
dword RowDataByte; //单块数据块字节数
qword OffsetAddress; //扩展线性地址
qword ReAddr; //上一数据行起始地址
dword Len; //HEX每行有效数据字节数
dword ReLen; //HEX前一次数据长度
dword Addr; //HEX每行起始地址
dword Type; //HEX每行类型,有00,01,04四种类型
RowDataByte = 0;i = 0;Len = 0;ReLen = 0;Addr=0;Type = 0;ReAddr = 0;
file_handle = OpenFileRead(Filename,0);
HexBlockTotalNumber = 0;
if(file_handle!=0)
{ // Read all lines
while ( fileGetStringSZ(RowData,elcount(RowData),file_handle)!=0 ){
//判断首字符是否为:号
if(RowData[0] == ':'){
Len = (char2byte(RowData[1])*0x10+char2byte(RowData[2]));
Addr = char2byte(RowData[3])*0x1000+char2byte(RowData[4])*0x100+char2byte(RowData[5])*0x10+char2byte(RowData[6]);
Addr |= (OffsetAddress << 16);
Type = char2byte(RowData[7])*0x10+char2byte(RowData[8]);
//以下为打印解析的过程,打印解析时候的变量
//write("RowData:%s,HexBlockTotalNumber:%d,ReLen:%X,ReAddr:%X,Addr:%X,RowDataByte:%X",RowData,HexBlockTotalNumber,ReLen,ReAddr,Addr,RowDataByte);
switch(Type){
case 0x00: //数据
if (Addr > (ReLen + ReAddr)){ //判断为新数据块
if(RowDataByte == 0) //是否为首行数据字节数
{
hexfile[HexBlockTotalNumber].BlockStartAddr = Addr; //记录新数据块的起始地址
}
else //不是首行
{
hexfile[HexBlockTotalNumber].BlockDataLength = RowDataByte; //数据长度
RowDataByte = 0; //重新开始计数
hexfile[HexBlockTotalNumber].BlockStartAddr = Addr; //记录新数据块的起始地址

HexBlockTotalNumber++;
}
}
for(i = 0; i< Len ; i++)
{
//储存buffer,注意没有对crc进行校验。
hexfile[HexBlockTotalNumber].dataBuffer[RowDataByte++]=(char2byte(RowData[2*i+9])*0x10+char2byte(RowData[2*i+10]));
}
ReAddr = Addr; //保存当前地址,下一次使用
ReLen = Len; //保存当前长度,下一次使用
break;
case 0x04: //扩展线性地址记录
OffsetAddress = char2byte(RowData[9])*0x1000+char2byte(RowData[10])*0x100+char2byte(RowData[11])*0x10+char2byte(RowData[12]); //偏移地址
break;
case 0x01: //地址,结束
hexfile[HexBlockTotalNumber].BlockDataLength = RowDataByte; //数据长度
HexBlockTotalNumber++;
break;
}
}

}
write("Hex文件读取成功, 数据分块:%d",HexBlockTotalNumber);
for(i = 0; i < HexBlockTotalNumber; i++)
{
write("数据块:%d, 起始地址:0x%X, 结束地址:0x%X, 数据长度:%6d字节\r\n", i+1, hexfile[i].BlockStartAddr, hexfile[i].BlockStartAddr + hexfile[i].BlockDataLength - 1, hexfile[i].BlockDataLength);
}
fileClose(file_handle);
}
else{
write("OpenFileRead,error occurs");
}

}
on key 'f'
{
t1 = timeNow();
Read_hexFile("test.hex");
write("%d",t1-timeNow());
write(hexfile[0].dataBuffer);
}

write (byte v[]) {
int i;
for (i=0; i<48; i++) write("dataBuffer:%x",hexfile[0].dataBuffer[i]);
}


Elf

ELF(Executable and Linkable Format,可执行与可链接格式)是一种用于可执行文件、目标文件、共享库和核心转储的标准文件格式。它广泛应用于类Unix系统(如Linux、BSD)以及嵌入式系统中。

ELF文件的结构

ELF文件由以下几个主要部分组成:

  1. ELF头
  2. 程序头表
  3. 节头表
  4. 段和节

ELF头(ELF Header)

  • 作用:描述文件的基本信息,如文件类型、目标架构、程序入口地址等。
  • 重要字段
    • e_ident:魔数(Magic Number),标识文件为ELF格式。
    • e_type:文件类型(如可执行文件、共享库等)。
    • e_machine:目标架构(如x86、ARM)。
    • e_entry:程序入口地址。
    • e_phoff:程序头表偏移。
    • e_shoff:节头表偏移。

程序头表(Program Header Table)

  • 作用:描述段(Segment)信息,用于加载可执行文件。
  • 重要字段
    • p_type:段类型(如可加载段、动态链接信息)。
    • p_offset:段在文件中的偏移。
    • p_vaddr:段在内存中的虚拟地址。
    • p_filesz:段在文件中的大小。
    • p_memsz:段在内存中的大小。

节头表(Section Header Table)

  • 作用:描述节(Section)信息,用于链接和调试。
  • 重要字段
    • sh_name:节名称(在字符串表中的索引)。
    • sh_type:节类型(如代码节、数据节)。
    • sh_addr:节在内存中的地址。
    • sh_offset:节在文件中的偏移。
    • sh_size:节的大小。

段(Segments)和节(Sections)

  • :用于加载和执行,通常包含代码、数据等。
  • :用于链接和调试,通常包含符号表、字符串表等。

ELF文件的类型

ELF文件可以分为以下几种类型:

  • 可执行文件(Executable):可以直接运行的程序。
  • 共享库(Shared Library):动态链接库(如.so文件)。
  • 目标文件(Relocatable File):编译生成的中间文件(如.o文件)。
  • 核心转储(Core Dump):程序崩溃时的内存转储。

ELF文件的工具

以下工具可以用于分析和操作ELF文件:

  • readelf:查看ELF文件的详细信息。
    • 示例:readelf -h <file>(查看ELF头)。
  • objdump:反汇编和查看ELF文件内容。
    • 示例:objdump -d <file>(反汇编代码段)。
  • nm:查看ELF文件的符号表。
    • 示例:nm <file>
  • strip:删除ELF文件的符号表和调试信息。
    • 示例:strip <file>

ELF文件的加载和执行

当ELF可执行文件被加载到内存中时,操作系统会执行以下步骤:

  1. 解析ELF头,确定文件类型和入口地址。
  2. 根据程序头表,将段加载到内存中。
  3. 设置程序的堆栈和环境变量。
  4. 跳转到入口地址,开始执行程序。

ELF文件的示例解析

以下是一个简单的ELF文件解析示例:

查看ELF头

1
readelf -h hello

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400430
Start of program headers: 64 (bytes into file)
Start of section headers: 6936 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28

查看程序头表

1
readelf -l hello

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
Elf file type is EXEC (Executable file)
Entry point 0x400430
There are 9 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000006fc 0x00000000000006fc R E 200000
LOAD 0x0000000000000700 0x0000000000600700 0x0000000000600700
0x0000000000000230 0x0000000000000238 RW 200000
...

查看节头表

1
readelf -S hello

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
There are 31 section headers, starting at offset 0x1b18:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
...

ELF文件的常见节

  • .text:代码段,包含程序的指令。
  • .data:已初始化的全局变量和静态变量。
  • .bss:未初始化的全局变量和静态变量。
  • .rodata:只读数据(如字符串常量)。
  • .symtab:符号表。
  • .strtab:字符串表。
  • .dynamic:动态链接信息。

DLL

DLL动态链接库,Dynamic Link Library)是 Windows 操作系统中一种重要的可执行文件格式,它包含可由多个程序同时使用的代码和数据。

[!tip]

  • 静态链接:将库代码直接复制到每个应用程序中
  • 动态链接:多个程序共享同一个 DLL 文件,在运行时调用
  • 扩展名通常为 .dll
  • 不能直接运行,需要由其他程序调用
  • 支持代码重用和模块化开发
  • 节省磁盘空间和内存

工作原理为:

1
2
3
应用程序 → 调用 → DLL 文件
↑ ↓
└──── 共享功能 ───┘

生成

我们现在想要生成一个简单的计算函数:

MathFunctions.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "pch.h"

int add(int a, int b)
{
return a + b;
}

int minus(int a, int b)
{
return a - b;
}

int multiply(int a, int b)
{
return a * b;
}

double divide(int a, int b)
{
double m = (double)a / b;
return m;
}

头文件pch.h如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef PCH_H
#define PCH_H

#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include <windows.h>

#endif //PCH_H

//定义宏
#ifdef IMPORT_DLL
#define API_MODE _declspec(dllimport)
#else
#define API_MODE _declspec(dllexport)
#endif

#ifdef __cplusplus
extern "C" { // C++中使用C链接约定,防止函数名被修饰
#endif

API_MODE int add(int a, int b);
API_MODE int minus(int a, int b);
API_MODE int multiply(int a, int b);
API_MODE double divide(int a, int b);

#ifdef __cplusplus
}
#endif

[!tip]

__declspec(dllexport/dllimport)

  • dllexport:标记函数/变量需要从DLL导出
  • dllimport:标记函数/变量需要从DLL导入

需要注意的是,这里的import指的是需要从dll导入,意思是外部在调用函数或者变量的时候需要加载dll,所以importexport是从外部函数的角度描述的。

extern "C"

  • 防止C++编译器对函数名进行名称修饰(name mangling)
  • 确保函数名在DLL中保持原样,便于动态加载

Vs studio

  1. 创建dll项目

    创建DLL新项目

    注意,这里记住dll的路径,防止之后找不到,可以勾选将解决方案和项目放在同一目录下,这样就不会找不到文件了

    配置项目属性

  2. 工程目录

    新建的工程目录为:

    1
    2
    3
    4
    5
    6
    7
    8
    Dll1.slnx
    Dll1.vcxproj
    Dll1.vcxproj.filters
    Dll1.vcxproj.user
    dllmain.cpp
    framework.h
    pch.cpp
    pch.h

    我们需要关注的是头文件和源文件,注意,这里如果外部想要用这个函数,在源文件中需要定义IMPORT_DLL

  3. 编译生成

    完成代码编写之后,右键项目,点击生成即可生成dll

    生成dll

    生成的dll文件位于Dll1\x64\Debug文件夹下。

shell

  1. vs studio

    1
    2
    # 使用命令行(需要安装Visual Studio开发人员命令提示符)
    cl /LD MathFunctions.c /FeMathFunctions.dll
  2. Mingw

    1
    gcc -shared -o MathFunctions.dll MathFunctions.c -DIMPORT_DLL

    这里的-DIMPORT_DLL就是增加IMPORT_DLL宏的意思

    如果想要使用隐式链接,那么可能还需要生成lib文件

    1
    dlltool --dllname MyLibrary.dll --def MyLibrary.def --output-lib libMyLibrary.a
  3. Linux

    1
    2
    # 创建共享库
    gcc -shared -fPIC -o libmathfunctions.so MathFunctions.c -DIMPORT_DLL

测试

显式链接

1
2
3
4
5
6
#include <windows.h>

int main() {
HMODULE hDll = LoadLibrary("MathFunctions.dll"); // 手动加载
// 获取函数指针...
}
  • 运行时动态加载DLL
  • 需要 LoadLibrary()GetProcAddress()
  • 不需要 .lib 文件
  • 代码看起来更"手动"

我们可以使用以下代码测试我们的dll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <windows.h>

// 定义函数指针类型
typedef int (*AddFunc)(int, int);
typedef int (*MultiplyFunc)(int, int);

int main() {
printf("=== 显式链接测试 ===\n");

// 1. 手动加载DLL
HMODULE hDll = LoadLibrary("MathDLL.dll");
if (hDll == NULL) {
printf("加载DLL失败! 错误码: %d\n", GetLastError());
return 1;
}

printf("DLL加载成功\n");

// 2. 获取函数地址
AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");
MultiplyFunc multiply = (MultiplyFunc)GetProcAddress(hDll, "Multiply");

if (add == NULL || multiply == NULL) {
printf("获取函数地址失败! 错误码: %d\n", GetLastError());
FreeLibrary(hDll);
return 1;
}

// 3. 使用函数指针调用
int sum = add(10, 20);
int product = multiply(10, 20);

printf("10 + 20 = %d\n", sum);
printf("10 * 20 = %d\n", product);

// 4. 卸载DLL
FreeLibrary(hDll);
printf("DLL已卸载\n");

return 0;
}

显示链接编译命令最简单

1
gcc -o TestProgram.exe TestProgram.c

编译链接过程为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
编译时:
TestExplicit.c → 编译器 → TestExplicit.obj

链接器 → TestExplicit.exe
(不需要.lib文件)

运行时:
启动 TestExplicit.exe

手动 LoadLibrary("MathDLL.dll")

手动 GetProcAddress(hDll, "Add")

通过函数指针调用

隐式链接

1
2
3
4
5
6
7
#include "MathFunctions.h"  // 包含头文件
#pragma comment(lib, "MathFunctions.lib") // 链接库

int main() {
int result = Add(10, 5); // 直接调用
printf("%d", result);
}
  • 编译器在编译时就确定函数地址
  • 需要 .lib 文件(导入库)
  • 程序启动时自动加载DLL
  • 代码看起来就像调用普通函数

隐式链接在编译阶段就指定好了dll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <windows.h>

// 声明DLL中的函数(通常放在头文件中)
// 链接器需要这些函数的实现,它们会在DLL中找到
__declspec(dllimport) int Add(int a, int b);
__declspec(dllimport) int Multiply(int a, int b);

int main() {
printf("=== 隐式链接测试 ===\n");

// 看起来直接调用,实际上链接器会处理
int sum = Add(10, 20);
int product = Multiply(10, 20);

printf("10 + 20 = %d\n", sum);
printf("10 * 20 = %d\n", product);

return 0;
}
1
gcc -o TestImplicit.exe TestImplicit.c -L. -lMathFunctions

这里的MathFunctionsliba文件

隐式链接编译链接的流程为:

1
2
3
4
5
6
7
8
9
编译时:
TestImplicit.c → 编译器 → TestImplicit.obj

MathDLL.lib(导入库) → 链接器 → TestImplicit.exe

运行时:
启动 TestImplicit.exe → 系统自动加载 MathDLL.dll

直接调用函数 Add(10, 20)