代码重定位详解
文章目录
- 一、段的概念以及重定位的引入
- 1.1 问题的引入
- 1.2 段的概念
- 1.3 重定位
- 二、如何实现重定位
- 2.1 程序中含有什么?
- 2.2 谁来做重定位?
- 2.3 怎么做重定位和清除BSS段?
- 2.4 加载地址和链接地址的区别
- 三、散列文件使用与分析
- 3.1 重定位的实质: 移动数据
- 3.2 散列文件示例
- 3.2.1 示例代码
- 3.2.2 散列文件语法
- 3.3 散列文件解析
- 3.4 怎么获得region的信息
- 3.4.1 可执行域的信息
- 3.4.2 加载域的信息
- 3.4.3 汇编代码里怎么使用这些信息
- 3.4.4 C语言里怎么使用这些信息
- 1.方法1
- 2.方法2
- 四、清除BSS段(ZI段)
- 4.1 C语言中的BSS段
- 4.2 清除BSS段
- 4.2.1 BSS段的位置和大小
- 4.2.2 怎么清除BSS段
- 1.汇编码
- 2.C语言
- 五、代码段重定位
- 5.1 加载地址等于链接地址
- 5.2 加载地址不等于链接地址
- 5.3 代码段不重定位的后果
- 5.4 代码段重定位
- 5.4.1 代码段的位置和大小
- 5.4.2 怎么重定位
- 1.汇编代码
- 2.C语言代码
- 5.5 为什么重定位之前的代码也可以正常运行?
- 五、重定位的纯C函数实现
- 5.1 难点
- 5.2 怎么理解上述代码
一、段的概念以及重定位的引入
1.1 问题的引入
复制之前的串口程序,修改为:”01_uart_question“,添加全局变量,把它打印出来,看看会发生什么事。
#include "uart.h"char g_char1 = 'A';
const char g_char2 = 'B';void delay(int d)
{while(d--);
}int main()
{char c;uart_init();putchar('l');putchar('j');putchar('g');putchar('6');putchar('6');putchar('6');putchar('\n');putchar('\r');putchar(g_char1);putchar(g_char2);while (1){c = getchar();putchar(c);putchar('\n');putchar('\r');putchar(c+1); putchar('\n');putchar('\r'); }return 0;
}
可以看见,我们在定义了g_char1和g_char2(定义为常量)后,分别打印时g_char1显示的是乱码,而g_char2显示正常。
为了研究该问题,我们写了一个string.c和string.h,里面是打印函数,我们将该文件包含进该工程中,查看g_char1和g_char2的地址。
由图可见,g_char2指向ROM,且表现为只读,所以能被成功打印,而g_char1指向内存RAM,该区域是可读可写的,而我们对于内存未进行赋值,所以当访问到g_char1指向的地址时,就会打印乱码(该内存中的任意值)。
1.2 段的概念
代码段、只读数据段、可读可写的数据段、BSS段。
char g_Char = 'A'; // 可读可写,不能放在ROM上,应该放在RAM里
const char g_Char2 = 'B'; // 只读变量,可以放在ROM上
int g_A = 0; // 初始值为0,干嘛浪费空间保存在ROM上?没必要
int g_B; // 没有初始化,干嘛浪费空间保存在ROM上?没必要
所以,程序分为这几个段:
- 代码段(RO-CODE):就是程序本身,不会被修改
- 可读可写的数据段(RW-DATA):有初始值的全局变量、静态变量,需要从ROM上复制到内存
- 只读的数据段(RO-DATA):可以放在ROM上,不需要复制到内存
- BSS段或ZI段:
- 初始值为0的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以
- 未初始化的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以
- 局部变量:保存在栈中,运行时生成
- 堆:一块空闲空间,使用malloc函数来管理它,malloc函数可以自己写
1.3 重定位
保存在ROM上的全局变量的值,在使用前要复制到内存,这就是数据段重定位。
想把代码移动到其他位置,这就是代码重定位。
二、如何实现重定位
2.1 程序中含有什么?
- 代码段:如果它不在链接地址上,就需要重定位
- 只读数据段:如果它不在链接地址上,就需要重定位
- 可读可写的数据段:如果它不在链接地址上,就需要重定位
- BSS段:不需要重定位,因为程序里根本不保存BSS段,使用前把BSS段对应的空间清零即可
2.2 谁来做重定位?
- 程序本身:它把自己复制到链接地址去
- 一开始,程序可能并不位于它的链接地址上,为什么它可以执行重定位的操作?
- 因为重定位的代码是使用“位置无关码”写的
- 什么叫位置无关码:这段代码扔在任何位置都可以运行,跟它所在的位置无关
- 怎么写出位置无关码:
- 跳转:使用相对跳转指令,不能使用绝对跳转指令
- 只能使用branch指令(比如
bl main
),不能给PC直接复制,比如ldr pc, =main
- 只能使用branch指令(比如
- 不要访问全局变量、静态变量
- 不使用字符串
- 跳转:使用相对跳转指令,不能使用绝对跳转指令
2.3 怎么做重定位和清除BSS段?
- 核心:复制
- 复制的三要素:源、目的、长度
- 怎么知道代码段/数据段保存在哪?(加载地址)
- 怎么知道代码段/数据段要被复制到哪?(链接地址)
- 怎么知道代码段/数据段的长度?
- 怎么知道BSS段的地址范围:起始地址、长度?
- 这一切
- 在keil中使用散列文件(Scatter File)来描述
- 在GCC中使用链接脚本(Link Script)来描述
2.4 加载地址和链接地址的区别
程序运行时,应该位于它的链接地址处,因为:
- 使用函数地址时用的是"函数的链接地址",所以代码段应该位于链接地址处
- 去访问全局变量、静态变量时,用的是"变量的链接地址",所以数据段应该位于链接地址处
但是: 程序一开始时可能并没有位于它的"链接地址":
- 比如对于STM32F103,程序被烧录器烧写在Flash上,这个地址称为"加载地址"
- 比如对于IMX6ULL/STM32MP157,片内ROM根据头部信息把程序读入内存,这个地址称为“加载地址”
当加载地址 != 链接地址时,就需要重定位。
三、散列文件使用与分析
3.1 重定位的实质: 移动数据
把代码段、只读数据段、数据段,移动到它的链接地址处(mcmcpy)。
也就是复制!
数据复制的三要素:源、目的、长度。
-
数据保存在哪里?加载地址
-
数据要复制到哪里?链接地址
-
长度
这3要素怎么得到?
在keil中,使用散列文件来描述。
散列?分散排列?
是的,在STM32F103这类资源紧缺的单片机芯片中,
- 代码段保存在Flash上,直接在Flash上运行(当然也可以重定位到内存里)
- 数据段保存在Flash上,使用前被复制到内存里
3.2 散列文件示例
3.2.1 示例代码
复制”01_uart_question“,改名为”02_uart_sct“。
在 Keil 中进行如下配置:
会生成一个.sct结尾的文件,使用Nope++打开:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************LR_IROM1 0x08000000 0x00080000 { ; load region size_regionER_IROM1 0x08000000 0x00080000 { ; load address = execution address*.o (RESET, +First).ANY (+RO).ANY (+XO)}RW_IRAM1 0x20000000 0x00010000 { ; RW data.ANY (+RW +ZI)}
}
3.2.2 散列文件语法
一个散列文件由一个或多个Load region(加载域)
组成:
load_region_description ::=load_region_name (base_address | ("+" offset)) [attribute_list] [max_size]"{"execution_region_description+"}
Load region
中含有一个或多个Execution region(可执行域)
,
Execution region
语法如下:
execution_region_description ::=exec_region_name (base_address | "+" offset) [attribute_list] [max_size | length]"{"input_section_description*"}
Execution region
中含有一个或多个Input section
,
Input section
语法如下:
input_section_description ::=
module_select_pattern [ "(" input_section_selector ( ","
input_section_selector )* ")" ]
input_section_selector ::=
"+" input_section_attr |
input_section_pattern |
input_section_type |
input_symbol_pattern |
section_properties
3.3 散列文件解析
*.o :所有objects文件
*:所有objects文件和库,在一个散列文件中只能使用一个*
.ANY:等同于*,优先级比*低;在一个散列文件的多个可执行域中可以有多个.ANY
.ANY (+RO):所有.o文件、库文件的只读数据段
.ANY (+XO):所有.o文件、库文件的可执行数据段
.ANY (+RW +ZI):所有.o文件、库文件的可读可写段和BSS段
3.4 怎么获得region的信息
3.4.1 可执行域的信息
3.4.2 加载域的信息
3.4.3 汇编代码里怎么使用这些信息
Load$$region_name$$Base //下面修改时候要注意region_name(区域名)
通过上图可知,可执行域1的源地址和目的地址一致,所以能够正常执行,不需要重定位;而可执行域2的源地址紧随1后,但其目的地址与源地址不同,所以需要对可执行域进行重定位。所以需要将区域名字修改成 RW_IRAM1 。
示例代码如下:
string.c中的memcpy函数:
void mcmcpy(void *dest, void *src, unsigned int len)
{unsigned char *pcDest;unsigned char *pcSrc;while (len--){*pcDest = *pcSrc; //让目的地与源地址相等,后面两者长度++,知道len=0,完成复制pcSrc++;pcDest++;}
}
IMPORT |Image$$RW_IRAM1$$Base|
IMPORT |Image$$RW_IRAM1$$Length|
IMPORT |Load$$RW_IRAM1$$Base|
IMPORT memcpyLDR R0, = |Image$$RW_IRAM1$$Base| ; DEST
LDR R1, = |Load$$RW_IRAM1$$Base| ; SORUCE
LDR R2, = |Image$$RW_IRAM1$$Length| ; LENGTH
BL memcpy
修改start.s,让目的地址与源地址一样后,编译烧录,可以发现A被成功打印。
3.4.4 C语言里怎么使用这些信息
1.方法1
声明为外部变量。
注意:使用时需要使用取址符:
extern int Image$$RW_IRAM1$$Base;
extern int Load$$RW_IRAM1$$Base;
extern int Image$$RW_IRAM1$$Length;memcpy(&Image$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length, &Load$$RW_IRAM1$$Base);
2.方法2
声明为外部数组。
注意:使用时不需要使用取址符:
extern char Image$$RW_IRAM1$$Base[];
extern char Load$$RW_IRAM1$$Base[];
extern int Image$$RW_IRAM1$$Length;memcpy(Image$$RW_IRAM1$$Base, Image$$RW_IRAM1$$Length, &Load$$RW_IRAM1$$Base);
四、清除BSS段(ZI段)
4.1 C语言中的BSS段
程序里的全局变量,如果它的初始值为0,或者没有设置初始值,这些变量被放在BSS段里,也叫ZI段。
char g_Char = 'A';
const char g_Char2 = 'B';
int g_A = 0; // 放在BSS段
int g_B; // 放在BSS段
BSS段并不会放入bin文件中,否则也太浪费空间了。
在使用BSS段里的变量之前,把BSS段所占据的内存清零就可以了。
-
注意:对于keil来说,一个本该放到BSS段的变量,如果它所占据的空间小于等于8字节自己,keil仍然会把它放在data段里。只有当它所占据的空间大于8字节时,才会放到BSS段。
int g_A[3] = {0, 0}; // 放在BSS段 char g_B[9]; // 放在BSS段int g_A[2] = {0, 0}; // 放在data段 char g_B[8]; // 放在data段
4.2 清除BSS段
4.2.1 BSS段的位置和大小
在散列文件中,BSS段(ZI段)在可执行域RW_IRAM1
中描述:
LR_IROM1 0x08000000 0x00080000 { ; load region size_regionER_IROM1 0x08000000 0x00080000 { ; load address = execution address*.o (RESET, +First)*(InRoot$$Sections).ANY (+RO).ANY (+XO)}RW_IRAM1 0x20000000 0x00010000 { ; RW data.ANY (+RW +ZI)}
}
BSS段(ZI段)的链接地址(基地址)、长度,使用下面的符号获得:
4.2.2 怎么清除BSS段
1.汇编码
string.c中的memset函数:
void memset(void *dest, unsigned char val, unsigned int len)
{unsigned char *pcDest = dest;while (len --){*pcDest = val;pcDest++;}
}
IMPORT |Image$$RW_IRAM1$$ZI$$Base| ;IMPORT在这里相当于定义
IMPORT |Image$$RW_IRAM1$$ZI$$Length|
IMPORT memsetLDR R0, = |Image$$RW_IRAM1$$ZI$$Base| ; DEST
MOV R1, #0 ; VAL:VAL为0 并赋给dest 完成清零操作
LDR R1, = |Image$$RW_IRAM1$$ZI$$Length| ; Length
BL memset
调用memset函数后,可见放入BSS段成功被清除为0:
2.C语言
- 方法1
声明为外部变量,使用时需要使用取址符:
extern int Image$$RW_IRAM1$$ZI$$Base;
extern int Image$$RW_IRAM1$$ZI$$Length;memset(&Image$$RW_IRAM1$$ZI$$Base, 0, &Image$$RW_IRAM1$$ZI$$Length);
- 方法2
声明为外部数组,使用时不需要使用取址符:
extern char Image$$RW_IRAM1$$ZI$$Base[];
extern int Image$$RW_IRAM1$$ZI$$Length[];memset(Image$$RW_IRAM1$$ZI$$Base[], 0, Image$$RW_IRAM1$$ZI$$Length);
五、代码段重定位
5.1 加载地址等于链接地址
在默认散列文件中,代码段的load address = execution address
,
也就是加载地址和执行地址(链接地址)一致,所以无需重定位:
LR_IROM1 0x08000000 0x00080000 { ; load region size_regionER_IROM1 0x08000000 0x00080000 { ; load address = execution address*.o (RESET, +First)*(InRoot$$Sections).ANY (+RO).ANY (+XO)}RW_IRAM1 0x20000000 0x00010000 { ; RW data.ANY (+RW +ZI)}
}
5.2 加载地址不等于链接地址
有时候,我们需要把程序复制到内存里运行,比如:
- 想让程序执行得更快:需要把代码段复制到内存里
- 程序很大,保存在片外SPI Flash中,SPI Flash上的代码无法直接执行,需要复制到内存里
这时候,需要修改散列文件,把代码段的可执行域放在内存里。
那么程序运行时,需要尽快把代码段重定位到内存。
散列文件示例:
LR_IROM1 0x08000000 0x00080000 { ; load region size_regionER_IROM1 0x20000000 { ; load address != execution address*.o (RESET, +First).ANY (+RO).ANY (+XO)}RW_IRAM1 +0 { ; RW data.ANY (+RW +ZI)}
}
上面的散列文件中:
- 可执行域ER_IROM1
- 加载地址为0x08000000,可执行地址为0x20000000,两者不相等
- 板子上电后,从0x08000000处开始运行,需要尽快把代码段复制到0x20000000
- 可执行域RW_IRAM1
- 加载地址:紧跟着ER_IROM1的加载地址(+0)
- 可执行地址:紧跟着ER_IROM1的可执行地址(+0)
- 需要尽快把数据段复制到可执行地址处
数据段的重定位我们做过实验,
如果代码段不重定位的话,会发生什么事?
修改完上面的散列文件,发现程序编译烧录后在Mobe软件上毫无反应,说明程序发生崩溃。我们进行解析:
我们程序运行时,在start.s中,第一条指令是从不是从Reset_Handler
开始执行,而是从__Vectors
,对于__Vectors的第一个数据DCD是用来设置栈,但是我们并不需要第一个数据,我们自己手动设置了栈
(LDR SP, =(0x20000000+0x10000)),对于第二个数据,CPU会在DCD中取出Reset_Handler这个值,并将其作为函数地址跳转过去。为了了解函数地址,我们打开反汇编代码,可以看见其对应的地址为20000009
,而我们的代码烧写在Flash上时,在开始是0,CPU一上电会用第一个值设置栈,但我们没有使用,我们后面自己去设置栈,在第二个位置取出该值,将其作为函数地址跳过去执行(注意:这里的20000009中的bit0并不使用,bit0只是用来表示跳过去后的指令为Tumb指令集),会跳到Reset Handler对应的20000008,但在20000008上,我们并未对该内存进行任何操作,此时如果PC跳到位置则会崩溃,由第三张图可见Reset_Handler对应的是08000008,所以我们需要在20000009的位置放入08000009(设置成09是因为bit(0)等于1表示后面执行的是Tumb指令集)。修改完毕后程序得以正常执行,成功清除ss段:
5.3 代码段不重定位的后果
不能使用链接地址来调用函数
-
汇编中
ldr pc, =main ; 这样调用函数时,用到main函数的链接地址,如果代码段没有重定位,则跳转失败
-
C语言中
void (*funcptr)(const char *s, unsigned int val); funcptr = put_s_hex; funcptr("hello, test function ptr", 123);
5.4 代码段重定位
5.4.1 代码段的位置和大小
在散列文件中,代码段在可执行域ER_IROM1
中描述:
LR_IROM1 0x08000000 0x00080000 { ; load region size_regionER_IROM1 0x08000000 0x00080000 { ; load address = execution address*.o (RESET, +First)*(InRoot$$Sections).ANY (+RO).ANY (+XO)}RW_IRAM1 0x20000000 0x00010000 { ; RW data.ANY (+RW +ZI)}
}
代码段的链接地址(基地址)、长度,使用下面的符号获得:
代码段的加载地址,使用下面的符号获得:
5.4.2 怎么重定位
1.汇编代码
把代码段从Flash复制到内存中:
IMPORT |Image$$ER_IROM1$$Base| ;IMPORT在这里相当于定义
IMPORT |Image$$ER_IROM1$$Length|
IMPORT |Load$$ER_IROM1$$Base|LDR R0, = |Image$$ER_IROM1$$Base| ; DEST
LDR R1, = |Load$$ER_IROM1$$Base| ; SORUCE
LDR R2, = |Image$$ER_IROM1$$Length| ; LENGTH
BL memcpy
注意:第二段代码已经进行重定位,而第一段代码还位于Flash上,为什么该段代码还没位于他的链接地址上却可以正常执行,因为该段代码使用的是位置无关码。所谓位置无关就是放在任何地方都能得以执行,跟他的位置没有任何关系。关键在于跳转,他使用的是相对跳转指令,而第二段代码使用的是链接地址跳转指令。
2.C语言代码
- 方法1
声明为外部变量,使用时需要使用取址符:
extern int Image$$ER_IROM1$$Base;
extern int Load$$ER_IROM1$$Base;
extern int Image$$ER_IROM1$$Length;memcpy(&Image$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);
- 方法2
声明为外部数组,使用时不需要使用取址符:
extern char Image$$ER_IROM1$$Base[];
extern char Load$$ER_IROM1$$Base[];
extern int Image$$ER_IROM1$$Length;memcpy(Image$$ER_IROM1$$Base, Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);
5.5 为什么重定位之前的代码也可以正常运行?
因为重定位之前的代码是使用位置无关码写的:
-
只使用相对跳转指令:B、BL
-
不只用绝对跳转指令:
LDR R0, =main BLX R0
-
不访问全局变量、静态变量、字符串、数组
-
重定位完后,使用绝对跳转指令跳转到XXX函数的链接地址去
BL main ; bl相对跳转,程序仍在Flash上运行LDR R0, =main ; 绝对跳转,跳到链接地址去,就是跳去内存里执行 BLX R0
五、重定位的纯C函数实现
5.1 难点
难点在于,怎么得到各个域的加载地址、链接地址、长度。
- 方法1
声明为外部变量,使用时需要使用取址符:
extern int Image$$ER_IROM1$$Base;
extern int Load$$ER_IROM1$$Base;
extern int Image$$ER_IROM1$$Length;memcpy(&Image$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);
- 方法2
声明为外部数组,使用时不需要使用取址符:
extern char Image$$ER_IROM1$$Base[];
extern char Load$$ER_IROM1$$Base[];
extern int Image$$ER_IROM1$$Length;memcpy(Image$$ER_IROM1$$Base, Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);
5.2 怎么理解上述代码
对于这样的C变量:
int g_a;
编译的时候会有一个符号表(symbol table),如下:
Name | Address |
---|---|
g_a | xxxxxxxx |
对于散列文件中的各类Symbol,有2中声明方式:
extern int Image$$ER_IROM1$$Base; // 声明为一般变量(包括int、char还有指针)
extern char Image$$ER_IROM1$$Base[]; // 声明为数组
不管是哪种方式,它们都会保存在符号表里,比如:
Name | Address |
---|---|
g_a | xxxxxxxx |
Image E R I R O M 1 ER_IROM1 ERIROM1Base | yyyyyyyy |
- 对于
int g_a
变量- 使用
&g_a
得到符号表里的地址。
- 使用
- 对于
extern int Image$$ER_IROM1$$Base
变量- 要得到符号表中的地址,也是使用
&Image$$ER_IROM1$$Base
。
- 要得到符号表中的地址,也是使用
- 对于
extern char Image$$ER_IROM1$$Base[]
变量- 要得到符号表中的地址,直接使用
Image$$ER_IROM1$$Base
,不需要加&
- 为什么?
mage$$ER_IROM1$$Base
本身就表示地址啊
- 要得到符号表中的地址,直接使用
示例代码:
在start.s中定义SystemInt,并调用跳转:
PRESERVE8THUMB; Vector Table Mapped to Address 0 at ResetAREA RESET, DATA, READONLYEXPORT __Vectors__Vectors DCD 0 DCD 0x08000009 ; Reset HandlerAREA |.text|, CODE, READONLY; Reset handler
Reset_Handler PROCEXPORT Reset_Handler [WEAK]IMPORT mymainIMPORT SystemInit ;定义SystemInitLDR SP, =(0x20000000+0x10000)BL SystemInit ;跳转SystemInit;BL mymainLDR R0, =mymainBLX R0ENDPEND
创建一个init.c,在其中写入c函数的重定位(包括int、char和指针):
#include "string.h"#if 0
void SystemInit()
{ extern int Image$$ER_IROM1$$Base;extern int Image$$ER_IROM1$$Length;extern int Load$$ER_IROM1$$Base;extern int Image$$RW_IRAM1$$Base;extern int Image$$RW_IRAM1$$Length;extern int Load$$RW_IRAM1$$Base;extern int Image$$RW_IRAM1$$ZI$$Base;extern int Image$$RW_IRAM1$$ZI$$Length;/* 代码段重定位 */memcpy(&Image$$ER_IROM1$$Base, &Load$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length); //目的地址、源地址、长度/* 数据段重定位 */memcpy(&Image$$RW_IRAM1$$Base, &Load$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length); //目的地址、源地址、长度/* 清除BSS段 */memset(&Image$$RW_IRAM1$$ZI$$Base, 0, &Image$$RW_IRAM1$$ZI$$Length); //源地址、清除为0、长度
}
#endif#if 0
void SystemInit()
{ extern int Image$$ER_IROM1$$Base[];extern int Image$$ER_IROM1$$Length[];extern int Load$$ER_IROM1$$Base[];extern int Image$$RW_IRAM1$$Base[];extern int Image$$RW_IRAM1$$Length[];extern int Load$$RW_IRAM1$$Base[];extern int Image$$RW_IRAM1$$ZI$$Base[];extern int Image$$RW_IRAM1$$ZI$$Length[];/* 代码段重定位 */memcpy(Image$$ER_IROM1$$Base, Load$$ER_IROM1$$Base, Image$$ER_IROM1$$Length); //目的地址、源地址、长度/* 数据段重定位 */memcpy(Image$$RW_IRAM1$$Base, Load$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length); //目的地址、源地址、长度/* 清除BSS段 */memset(Image$$RW_IRAM1$$ZI$$Base, 0, Image$$RW_IRAM1$$ZI$$Length); //源地址、清除为0、长度
}
#endifvoid SystemInit()
{ extern int *Image$$ER_IROM1$$Base;extern int *Image$$ER_IROM1$$Length;extern int *Load$$ER_IROM1$$Base;extern int *Image$$RW_IRAM1$$Base;extern int *Image$$RW_IRAM1$$Length;extern int *Load$$RW_IRAM1$$Base;extern int *Image$$RW_IRAM1$$ZI$$Base;extern int *Image$$RW_IRAM1$$ZI$$Length;/* 代码段重定位 */memcpy(&Image$$ER_IROM1$$Base, &Load$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length); //目的地址、源地址、长度/* 数据段重定位 */memcpy(&Image$$RW_IRAM1$$Base, &Load$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length); //目的地址、源地址、长度/* 清除BSS段 */memset(&Image$$RW_IRAM1$$ZI$$Base, 0, &Image$$RW_IRAM1$$ZI$$Length); //源地址、清除为0、长度
}
三者结果一致:
相关文章:

代码重定位详解
文章目录 一、段的概念以及重定位的引入1.1 问题的引入1.2 段的概念1.3 重定位 二、如何实现重定位2.1 程序中含有什么?2.2 谁来做重定位?2.3 怎么做重定位和清除BSS段?2.4 加载地址和链接地址的区别 三、散列文件使用与分析3.1 重定位的实质…...

活动预告 | Microsoft 365 在线技术公开课:让组织针对 Microsoft Copilot 做好准备
课程介绍 通过Microsoft Learn免费参加Microsoft 365在线技术公开课,建立您需要的技能,以创造新的机会并加速您对Microsoft云技术的理解。参加我们举办的“让组织针对 Microsoft Copilot for Microsoft 365 做好准备” 在线技术公开课活动,学…...

从0到机器视觉工程师(一):机器视觉工业相机总结
目录 相机的作用 工业相机 工业相机的优点 工业相机的种类 工业相机知名品牌 光源与打光 打光方式 亮暗场照明 亮暗场照明的应用 亮暗场照明的区别 前向光漫射照明 背光照明 背光照明的原理 背光照明的应用 同轴光照明 同轴光照明的应用 总结 相机的作用 相机…...

Docker安装(Docker Engine安装)
一、Docker Engine和Desktop区别 Docker Engine 核心组件:Docker Engine是Docker的核心运行时引擎,负责构建、运行和管理容器。它包括守护进程(dockerd)、API和命令行工具客户端(docker)。适用环境&#…...

数组的深度监听deep
场景:组件提供的emit事件可能被占用,在不能使用事件提交的情况下,就要上watch数组监听了,但是是发现只有在数组的长度发生变化的时候才会触发监听,这怎么行。。。。。 对于对象数组的深度监听,如果没有正确…...

点击锁定按钮,锁定按钮要变成解锁按钮,然后状态要从待绑定变成 已锁定(升级版)
文章目录 1、updateInviteCodeStatus2、handleLock3、InviteCodeController4、InviteCodeService5、CrudRepository 点击锁定按钮,锁定按钮要变成解锁按钮,然后状态要从待绑定变成 已锁定:https://blog.csdn.net/m0_65152767/article/details…...

UniApp 性能优化策略
一、引言 在当今数字化时代,移动应用的性能成为影响用户留存与满意度的关键因素。UniApp 作为一款热门的跨平台开发框架,以一套代码适配多端的特性极大提升了开发效率,但同时也面临着性能优化的挑战。优化 UniApp 性能,不仅能够让…...

【Linux报告】实训六 重置超级用户密码
实训六 重置超级用户密码 2018编写-今日公布 【练习一】忘记root密码 步骤一:开启或重启系统,并且要在五秒之内按任何键; 步骤二:按任意键,停止进入系统,按【e】键,跳转新页面,再…...

smolagents:一个用于构建代理的简单库
HF推出 smolagents,一个非常简单的库,它能够解锁语言模型的代理功能。以下是它的简要介绍: from smolagents import CodeAgent, DuckDuckGoSearchTool, HfApiModelagent CodeAgent(tools[DuckDuckGoSearchTool()], modelHfApiModel())agent…...

通过Dockerfile来实现项目可以指定读取不同环境的yml包
通过Dockerfile来实现项目可以指定读取不同环境的yml包 1. 挂载目录2. DockerFile3. 运行脚本deploy.sh4. 运行查看日志进入容器 5. 接口测试修改application-dev.yml 6. 优化Dockerfile7. 部分参数解释8. 优化不同环境下的日志也不同调整 Dockerfile修改部署脚本 deploy.sh重新…...

云手机 —— 手机矩阵的 “超级外挂
如何打造手机矩阵 打造手机矩阵主要包括以下几个步骤: 1.确定目标与需求:首先,明确打造手机矩阵的目的和需求,是为了进行电商运营、自媒体推广、任务管理还是其他目的。这将决定后续的手机数量、操作系统选择以及应用安装等。 2.选择手机与操作系统:根据…...

OpenCV的TickMeter计时类
OpenCV的TickMeter计时类 1. TickMeter是一个计时的类1.1 计算耗时1.2 计算循环的平均耗时和FPS1.3 function 2. 案例 1. TickMeter是一个计时的类 https://docs.opencv.org/4.x/d9/d6f/classcv_1_1TickMeter.html#details 1.1 计算耗时 TickMeter tm;tm.start();// do some…...

蓝桥杯JAVA刷题--001
文章目录 题目需求2.代码3.总结 题目需求 2.代码 class Solution {public String convertDateToBinary(String date) {if (date null || date.length() ! 10 || date.charAt(4) ! - || date.charAt(7) ! -) {throw new IllegalArgumentException("输入的日期格式不正确&…...

免费又开源:企业级物联网平台的新选择 ThingsPanel
在开源领域,选择合适的开源协议是开发者和企业能否充分利用平台的关键。ThingsPanel,作为一个专注于物联网的开源平台,近日将协议从 AGPLv3 改为更开放的 Apache 2.0。这一改变对开发者和用户意味着什么? 为什么协议要从 AGPLv3 转…...

鸿蒙开发:文本合成语音
前言 Android开发的同学都知道,在Android当中,实现一段文字合成语音播放,可以使用系统提供的对象TextToSpeech来很快的实现,如果不用系统自带的,也可以使用三方提供的,比如讯飞的语音合成等等,总…...

雷军:科技传奇的逐梦之旅
亲爱的小伙伴们😘,在求知的漫漫旅途中,若你对深度学习的奥秘、Java 与 Python 的奇妙世界,亦或是读研论文的撰写攻略有所探寻🧐,那不妨给我一个小小的关注吧🥰。我会精心筹备,在未来…...

LeetCode - 初级算法 数组(删除排序数组中的重复项)
免责声明:本文来源于个人知识与公开资料,仅用于学术交流。 删除排序数组中的重复项 这篇文章讨论如何从一个非严格递增的数组 nums 中删除重复的元素,使每个元素只出现一次,并返回新数组的长度。因为数组是排序的,只要是相同的肯定是挨着的,所以我们需要遍历所有数组,然…...

2024年度培训运维总结
目前带的弟子跟我实战训练运维的总就业率基本可以稳定在 100% 左右(因为我不带小白和没学历和大龄,悟性低都筛掉了还能找我说明条件够了) 以下是弟子私教训练营的最新就业数据(2024年12月25日): 面试辅导…...

java重装小结
一、Java安装 安装路径 https://www.oracle.com/java/technologies/javase/javas e8-archive-downloads.html 具体类型可参考: Java安装配置-CSDN博客 我在这一步主要碰到的问题就是访问官网报404错误,可参考: 在oracle官网下载资源显示…...

ubuntu20.04 中文输入法安装
中文输入法安装指南 在 Ubuntu 20.04 上安装中文输入法,您可以按照以下步骤操作: 打开终端。安装 fcitx 输入法框架和中文输入法模块:sudo apt update sudo apt install fcitx fcitx-pinyin安装中文语言支持:sudo apt install l…...

SQL常用语句(基础)大全
SQL语句的类型 1.DDL 1.库2.表 2.DML 1.插入数据 insert inot2.删除数据 delete / truncate3.修改数据 update set 3.DQL 1.无条件查询2.查询 什么开始 到什么结束3.指定条件查询 1.单个条件 ro in2.多个条件 and4.查询不为NULL值 is not null ,为NULL值 is null5.模糊查询 li…...

计算和可视化相对湿度结果
了解如何确定 CFD 模型中的相对湿度。 了解相对湿度 大气是干燥空气和水蒸气的混合物,每一种的压力之和等于蒸气压。相对湿度是指空气中的水分量与空气在特定温度下可以容纳的最大水分量之比。它通常以百分比表示。它的范围从 0(干燥空气)到…...

uniapp-vue3(下)
关联链接:uniapp-vue3(上) 文章目录 七、咸虾米壁纸项目实战7.1.咸虾米壁纸项目概述7.2.项目初始化公共目录和设计稿尺寸测量工具7.3.banner海报swiper轮播器7.4.使用swiper的纵向轮播做公告区域7.5.每日推荐滑动scroll-view布局7.6.组件具名…...

一起学习Firtran: Fortran中的流程控制与操作符
流程控制语句 在计算机编程中,控制程序流程是算法设计中的一个核心概念,它使得程序能够根据特定条件执行不同的操作或重复执行某段代码。这显著增强了计算机算法相对于简单数学公式的功能和灵活性。 if语句 在单个 if 语句中只有当测试表达式࿰…...

Sonic:开源Go语言开发的高性能博客平台
Sonic:一个用Go语言开发的高性能博客平台 简介 Sonic,一个以其速度如声速般快速而命名的博客平台,是一个用Go语言开发的高性能博客系统。正如其名字所暗示的,Sonic旨在提供一个简单而强大的博客解决方案。这个项目受到了Halo项目…...

SpringBoot教程(十四) SpringBoot之集成Redis
SpringBoot教程(十四) | SpringBoot之集成Redis 一、Redis集成简介二、集成步骤 2.1 添加依赖2.2 添加配置2.3 项目中使用之简单使用 (举例讲解)2.4 项目中使用之工具类封装 (正式用这个)2.5 序列化 &…...

RSI和CCI指标组合:如何评估需求供应区?昂首资本实战指南
在市场分析中,分笔成交点数量提供了一个有价值的视角,帮助我们识别需求和供应区。通过这些数据,我们可以假设供需区域的参数与主要振荡器中的区域参数类似。以RSI和CCI为例,这些指标不仅能够反映市场的超买和超卖情况,…...

PTPVT 插值说明
文章目录 PTPVT 插值说明 PTPVT 插值说明PVT Hermite插值PVT 三次多项式插值PT 插值Sin轨迹测试结果PVT Hermite插值结果PVT 三次多项式插值结果PT 插值结果 用户轨迹测试结果PVT Hermite插值结果PT 插值结果 PTPVT 插值说明 PT模式: 位置-时间路径插值算法。 PVT模…...

Spring MVC和servlet
1.Spring MVC是Spring框架的一个扩展 2.Spring MVC工作流程 1、用户发送请求至前端控制器DispatcherServlet。 2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。 3、处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及…...

java下载文件流,不生成中间文件。
java下载文件流,不生成中间文件。 代码设计:代码实现 代码设计: 从前端获取的数据经过后端加工后,生成文件流,并返回前端,(不生成中间文件,注意内存,记得关闭流…...