C语言中计算机概念:
栈:
栈(stack)是软件中最重要的概念之一,几乎每一个程序都使用了栈,
没有栈就没有函数,没有局部变量. !
堆:
光有栈还远远不够, 因为栈在函数返回的时候就会被释放掉,无法用栈长久保持数据。
而全局变量没有办法动态地产生,只能在编译的时候定义,有很多情况下缺乏表现力。
在这种情况下,堆(Heap)是唯一的选择。!
I/O:
没有I/O 的程序是没有意义的! 除非是为了演示结构.
计算机的I/O代表了计算机与外界的交互,交互的对象可以是人或其他设备
程序的I/O代表了程序与外界的交互, 广义的理解为文件操作。
linux 认为一切i/o皆文件, 包括设备、磁盘、网络,管道,命令行等都用文件來抽象.
符号:
没有符号就没有办法进行连接.
每声明一个全局变量或静态变量对应一个符号,
每声明一个函数也对应一个符号.
每引用一个外部符号或外部函数也对应一个符号.
连接需要符号,动态连接需要动态符号.
用readelf -s 可以看得到目标文件中的符号!
符号有名,有值,FUNC 有size, FUNC 无size 是内部框架函数. 有index表明在哪里定义,
符号类型: ST_LOCAL, ST_GLOBAL, ST_SECTION, ST_FILE, ST_WEAK, ST_NOTYPE 及特定语意等.
忽然想到, 你声明的变量或者函数都对应一个符号, 不过有的是本地的,有的是全局的. 它还会加入
section, filename, 及辅助性的_end, _enddata 等符号.
符号结构定义:
typedef struct elf32_sym{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
符号的存在是为了连接的需要,
定义符号是为了引用符号, 引用符号就是使用符号的值.
本地符号连接本地,全局符号连接外地. 到底应该怎么连接,这依赖于具体的CPU类型及指令类型,数据类型.
如果都连接好了,符号就没有用了.
程序的运行并不需要符号, 所以执行程序或so可以strip symbol, 这样本地的symbol就会被去除,减少尺寸.
但是动态的符号不能去除,因为加载器要靠动态信息加载和连接外部模块.
全局符号介入:
考察一个运行文件, 它要引用用1个外部符号val,
printf("%d\n",val);
它加载了动态库so1, val在so1中定义 val = 1;
也加载了动态库so2, val在so2中也有定义 val = 2;
那这个val 到底是谁呢? 是第一个加载的 val = 1.
当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略!!!
这种一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖的现象被称为
共享对象全局符号介入(Global Symbol Interpose)。
所以共享库中对本模块全局变量和本模块全局函数的引用,都采用像引用外部变量和外部函数一样的方式.
因为它不知道被引用的目标在哪里. 它引用的全局符号名,可能并不是本模块的全局符号名.
尤其是,当一个可执行模块与共享模块连接时, 共享模块的全局变量,都被映射到执行模块的.bss 区了. 意思是说,
共享模块和执行模块引用的都是.bss的数据, 而共享模块定义的那个没有人用了!
如果想采用相对寻址方式, 可以把变量或函数声明为static 方式.
重定位:
连接模块认为,它们的起始地址开始于0, 实际上,它们的节会被重新组织.
共享模块认为它们的模块地址开始于0, 实际上它们会被加载到某一个不知道的地址.
重定位是"代码和数据位置的修正",PIC 要求代码不能再修正.
重定位是为了代码能够正确执行而存在. 连接的过程也就是重定位的过程.
静态连接把可连接模块连接成一个整体,一个模块或可执行文件, 动态连接(加载)把共享模块及运行文件连接成一个进程.
重定位的结构定义:(带addend 和 不带addend, 前者可包含后者).
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
r_info 是r_symbol(index)(高位) 和 r_type(低位) 的合成.
重定位类型是与CPU类型相关的, 不过有几类比较重要.
对外部数据,外部函数一般采用绝对地址, 绝对地址需要重定位.
对本地数据,本地函数调用一般采用相对地址. 相对地址(偏移量)不需要重定位.
本地数据指针, so 文件需要rebase.
c 代码的执行流程:
_start 调用 __lib_start_main函数
libc\sysdeps\i386\elf\Start.S:
_start:
xorl %ebp, %ebp ; ebp = 0
popl %esi ; esi 存入的是argc
movl %esp, %ecx ; ecx 存入的是argv 地址
; 参数入栈
pushl %esp ; top of stack
pushl %edx ; edx, rtld_fini(runtime_loader 收尾工作入口)
pushl $__libc_csu_fini ; fini , main 后的收尾工作
pushl $__libc_csu_init ; init , main 前的初始化
pushl %ecx ; argv
pushl %esi ; argc
pushl main ; 用户main函数地址
call __libc_start_main ; c 框架main 函数
hlt ; 永不会到达此处
__libc_start_main 对流程的控制.
-
before main:对运行库和程序运行环境进行初始化,
包括堆、I/O、线程、全局变量构造,等等。
堆:
堆的初始化实际上是libc向系统申请一块内存(批发), 然后可以零售给应用程序.
I/O:
i/o初始化, libc 与0,1,2文件描述符建立了关联,你可以使用printf, getc向屏幕输出和从键盘输入
线程安全…
代码全局初始化, 在.init 函数中. -
main,正式开始执行程序主体部分。
-
after main:进行清理工作,
包括全局变量析构、堆销毁、关闭I/O等, -
结束进程。
API 与 ABI
-
API: application interface.
平时所说的API, 就是编写代码时,如何调用库函数的接口. -
ABI: application binary interface.
ABI 是二进制模块是否能够兼容的问题.
一个二进制模块文件, 能不能被外部应用调用,要看ABI 是否合适.
c++ abi(c++ 兼容性不好或较难保持兼容性), c abi 较易保持兼容性
例如android-eabi, 就与gnu-eabi不同, 他们在运行库,标准库上都不兼容.
abi 可以狭义的理解为调用接口的实现方式, 包括cpu,寄存器,参数传递方式等使用约定
广义理解为二进制文件的运行环境.