bin文件是纯二进制文件,不包含任何里链接地址,符号表等信息,它的执行地址是你烧写bin文件的地址,烧写到哪个地址处就在那里执行;
elf文件的执行地址是在文件编译链接时候确定的,烧写的地址并不是执行的地址,加载器会解析elf文件里记录的执行地址,将其从烧写地址处拷贝到执行地址处执行;
程序执行的过程: 1、写好一个程序,经过编译、链接后会生成一个可执行文件,在linux平台下是ELF(Executable Linkable Format)格式的,windows平台下是PE(Portable Executable)格式的。 2、然后你执行这个可执行文件,这个可执行文件里面的代码段、数据段和BSS段会被加载到PC或者某设备的内存中。代码段里放的就是指令,所以内存里的指令是通过执行某可执行文件加载到内存里的。 3、CPU会从代码段的起始地址,调用第一条指令,开始执行。如果没有遇到跳转指令就顺序执行:假设代码段起始地址是0x100,那么就是先执行0x100这个地址里的指令,然后再执。原因就是硬盘和内存在传输之间是有不同的地方的。计算机所有设备各部件之间的延时排列由高到低,依次为机械硬盘、固态硬盘、存储器和CPU;从数据响应速度来看,存储器明显优于硬盘。数据的读写速度与固态磁盘的读写速度相差甚远。 实际上,计算机内存和CPU缓存的功能是一样的。实际上,它是CPU和硬盘之间的一个临时存储区。CPU需要访问和处理的数据将通过这里。当计算机工作时,首先将要使用的东西从硬盘调用到内存,然后根据情况在CPU中输入各级缓存,最后由CPU调用。 同时,内存也是数据临时存储的地方。例如,可以使用文本软件输入一段文本,也可以使用绘图软件绘制。在您按下保存按钮或软件帮助您自动保存之前。
计算机区分指令和数据有以下2种方法: 1、通过不同的时间段来区分指令和数据,即在取指令阶段(或取指微程序)取出的为指令,在执行指令阶段(或相应微程序)取出的即为数据。 2、通过地址来源区分,由PC提供存储单元地址的取出的是指令,由指令地址码部分提供存储单元地址的取出的是 *** 作数。 存储器中的每段存储空间都会有一个地址,每个指令都包括一段 *** 作数和一段空间地址,cpu会根据 *** 作数去处理地址所指的数据。 一般计算机先读取存储器最开始的内容(这一部分是指令),然后加载 *** 作系统(先是LOADER)后由 *** 作系统对硬盘文件系统结构(即是数据)以判断其他数据和指令的位置。
我们从一个简单的NDK Demo开始分析。
下面从 SystemloadLibrary() 开始分析。
下面看 loadLibrary0()
参数 loader 为Android的应用类加载器,它是 PathClassLoader 类型的对象,继承自 BaseDexClassLoader 对象,下面看 BaseDexClassLoader 的 findLibrary() 方法。
下面看 DexPathList 的 findLibrary() 方法
回到 loadLibrary0() ,有了动态库的全路径名就可以装载库了,下面看 doLoad() 。
nativeLoad() 最终调用 LoadNativeLibrary() ,下面直接分析 LoadNativeLibrary() 。
对于JNI注册,这里暂不讨论,下面看 OpenNativeLibrary() 的实现。
下面看 android_dlopen_ext() 的实现
接下来就Android链接器linker的工作了。
下面从 do_dlopen() 开始分析。
find_library() 当参数translated_name不为空时,直接调用 find_libraries() ,这是装载链接的关键函数,下面看它的实现。
find_libraries() 中动态库的装载可以分为两部分
下面从 find_library_internal() 开始分析。
下面分析 load_library()
下面看另一个 load_library() 的实现
下面分析ELF文件头以及段信息的读取过程,也就是LoadTask的 read() ,它直接调用ElfReader的 Read() 方法。
动态库的装载在LoadTask的 load() 中实现。
下面看ElfReader的 Load() 方法
动态库的装载已经完成,下面看链接过程。
下面看 prelink_image()
链接主要完成符号重定位工作,下面从 link_image() 开始分析
下面以函数引用重定位为例分析 relocate() 方法
一个程序内存分配:
下图是APUE中的一个典型C内存空间分布图(虚拟内存)
例如:
int g1=0, g2=0, g3=0;
int max(int i)
{
int m1=0,m2,m3=0, p_max;
static n1_max=0,n2_max,n3_max=0;
p_max = (int )malloc(10);
printf("打印max程序地址\n");
printf("in max: 0xx\n\n",max);
printf("打印max传入参数地址\n");
printf("in max: 0xx\n\n",&i);
printf("打印max函数中静态变量地址\n");
printf("0xx\n",&n1_max); //打印各本地变量的内存地址
printf("0xx\n",&n2_max);
printf("0xx\n\n",&n3_max);
printf("打印max函数中局部变量地址\n");
printf("0xx\n",&m1); //打印各本地变量的内存地址
printf("0xx\n",&m2);
printf("0xx\n\n",&m3);
printf("打印max函数中malloc分配地址\n");
printf("0xx\n\n",p_max); //打印各本地变量的内存地址
if(i) return 1;
else return 0;
}
int main(int argc, char argv)
{
static int s1=0, s2, s3=0;
int v1=0, v2, v3=0;
int p;
p = (int )malloc(10);
printf("打印各全局变量(已初始化)的内存地址\n");
printf("0xx\n",&g1); //打印各全局变量的内存地址
printf("0xx\n",&g2);
printf("0xx\n\n",&g3);
printf("======================\n");
printf("打印程序初始程序main地址\n");
printf("main: 0xx\n\n", main);
printf("打印主参地址\n");
printf("argv: 0xx\n\n",argv);
printf("打印各静态变量的内存地址\n");
printf("0xx\n",&s1); //打印各静态变量的内存地址
printf("0xx\n",&s2);
printf("0xx\n\n",&s3);
printf("打印各局部变量的内存地址\n");
printf("0xx\n",&v1); //打印各本地变量的内存地址
printf("0xx\n",&v2);
printf("0xx\n\n",&v3);
printf("打印malloc分配的堆地址\n");
printf("malloc: 0xx\n\n",p);
printf("======================\n");
max(v1);
printf("======================\n");
printf("打印子函数起始地址\n");
printf("max: 0xx\n\n",max);
return 0;
}
打印结果:
ELF目标文件格式的最前端是 ELF文件头(ELF Header) ,
包含了描述整个文件的基本属性,如ELF版本、目标机器型号、 程序入口地址 等
3 加载:
编译技术什么是程序的入口语句?我们刚开始学习C语言的时候,总会以一个hello world程序开始了解,并会被告知main是程序的入口函数。
那么是否有怀疑过,既然main是函数的入口,那么全局函数是如何初始化的?
实际上main并不是一个程序的入口地址。
以linux为例,一个可执行程序是ELF格式(Executable and Linkable Format)。在ELF的文件头里记录了当程序入口地址,该入口地址是内核加载该文件后的第一条指令执行的地方。
可以写个hello world,生成可执行文件,然后通过readelf -h aout查看文件头:
ELF 头: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 类别: ELF64 数据: 2 补码,小端序 (little endian) 版本: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 类型: DYN (共享目标文件) 系统架构: Advanced Micro Devices X86-64 版本: 0x1 入口点地址: 0x1040 程序头起点: 64 (bytes into file) Start of section headers: 14688 (bytes into file) 标志: 0x0 本头的大小: 64 (字节) 程序头大小: 56 (字节) Number of program headers: 11 节头大小: 64 (字节) 节头数量: 29 字符串表索引节头: 28
这里入口地址是0x1040。
让我们反编译看看:
0000000000001040 <_start>: 1040: f3 0f 1e fa endbr64 1044: 31 ed xor %ebp,%ebp 1046: 49 89 d1 mov %rdx,%r9 1049: 5e pop %rsi 104a: 48 89 e2 mov %rsp,%rdx 104d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 1051: 50 push %rax 1052: 54 push %rsp 1053: 4c 8d 05 76 01 00 00 lea 0x176(%rip),%r8 # 11d0 <__libc_csu_fini> 105a: 48 8d 0d ff 00 00 00 lea 0xff(%rip),%rcx # 1160 <__libc_csu_init> 1061: 48 8d 3d d1 00 00 00 lea 0xd1(%rip),%rdi # 1139 <main> 1068: ff 15 72 2f 00 00 callq 0x2f72(%rip) # 3fe0 <__libc_start_main@GLIBC_225> 106e: f4 hlt 106f: 90 nop
0x1040对应的是_start函数,_start调用了glibc中的__libc_strat_main__libc_strat_main最终会调用main函数。(_start和__libc_start_main的分析见《程序员的自我修改——链接、装载与库》111)
既然知道main不是程序的真正入口,那么是否有办法在main之前或之后执行代码呢?
gcc提供了一组“进入”、“退出”函数可以首先这个功能:
用于告诉汇编程序如何进行汇编的指令,它既不控制机器的 *** 作也不被汇编成机器代码,只能为汇编程序所识别并指导汇编如何进行。
ADRL
伪指令
将相对于程序或相对于寄存器的地址载入寄存器中。
与
ADR
指令相似。ADRL
所加载的地址比
ADR
所加载的地址更宽,因为它可生成两个数据处理指令。
Note
汇编版本老于
ARMv6T2
的处理器的
Thumb
指令时,ADRL
是无效的。
语法
ADRL{cond}
Rd,label
其中:
cond
是一个可选的条件代码(请参阅条件执行)。
Rd
是要加载的寄存器。
label
表达式,与程序或寄存器相关。
有关详细信息,请参阅相对寄存器和程序相对的表达式。
用法
ADRL
始终汇编为两个
32
位指令。
即使使用单个指令就可完成地址访问,也会生成多余的第二个地址。
如果汇编程序无法将地址构建为两个指令,则它将生成一条错误消息,汇编将失败。
有关加载更宽范围地址的信息,请参阅LDR
伪指令(另请参阅将常数加载到寄存器)。
ADRL
可生成与位置无关的代码,因为地址与程序或寄存器有关。
如果
label
与程序有关,则其表示的地址必须要与
ADRL
伪指令在同一汇编程序区域内,请参阅AREA。
如果使用
ADRL
来为
BX
或
BLX
指令生成目标,则当目标中包含
Thumb
指令时,您就要自己设置地址的
Thumb
位(位
0)。
体系结构和范围
可用范围取决于所用的指令集:
ARM
±64KB
到字节或半字对齐的地址。
±256KB
字节,字对齐地址。
32
位
Thumb
±1MB
字节,字节、半字或字对齐地址。
16
位
Thumb
ADRL
不可用。
上面给出的范围是相对于位于当前指令后的、离当前指令有四个字节(在
Thumb
代码中)或两个字(在
ARM
代码中)间隔的点而言的。
在
ARM
和
32
位
Thumb
中,如果地址为
16
字节对齐,或与该点的相对性更高,则相对地址的范围可更大。
MOV32
伪指令
将以下项之一加载到寄存器:
一个
32
位常数值
任何地址。
MOV32
始终会生成两个
32
位指令,即一个
MOV、MOVT
对。
您可利用它加载任何
32
位常数或访问整个地址空间。
如果用
MOV32
加载地址,则所生成的代码将与位置有关。
语法
MOV32{cond}
Rd,
expr
其中:
cond
是一个可选的条件代码(请参阅条件执行)。
Rd
是要加载数据的寄存器。Rd
不可为
sp
或
pc。
expr
可以是下列项之一:
symbol
程序区域中的标签。
constant
任何
32
位常数。
symbol
+
constant
标签加上
32
位常数。
用法
MOV32
伪指令的主要功能有:
当单个指令中无法生成立即数时,生成文字常数。
将相对于程序的地址或外部地址载入寄存器中。
无论链接器将包含
MOV32
的
ELF
代码段置于何处,该地址始终有效。
Note
以这种方式加载的地址是在链接时确定的,因此代码不是位置无关的。
如果所引用的标签位于
Thumb
代码中,则
MOV32
将会设置该地址的
Thumb
位(位
0)。
体系结构
此伪指令在
ARMv6T2
和
ARMv7
中的
ARM
和
Thumb
状态下均有效。
LDR
伪指令
将以下项之一载入寄存器:
一个
32
位常数值
一个地址。
Note
本节仅介绍
LDR
伪
指令。
有关
LDR
指令
的详细信息,请参阅
内存访问指令。
有关使用
LDR
伪指令加载常数的信息,请参阅用
LDR
Rd,
=const
加载。
语法
LDR{cond}{w}
Rt,=[expr
|
label‑expr]
其中:
cond
是一个可选的条件代码(请参阅条件执行)。
W
是可选的指令宽度说明符。
Rt
是要加载的寄存器。
以上就是关于使用不同的arm-linux-ld -tText 链接地址,但是编译出来的bin文件一致全部的内容,包括:使用不同的arm-linux-ld -tText 链接地址,但是编译出来的bin文件一致、为什么编好的程序和原始数据得事先存到存储器中,cpu才能够从地址当中加载指令、Android Native库的加载及动态链接等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)