跳到主要内容

13-15 深入理解程序

13 深入理解程序的结构

深入理解程序的结构

程序由不同的段构成(代码段,数据段)

  • 程序的静态特征就是指令和数据
  • 程序的动态特征就是执行指令处理数据

源程序代码到可执行程序文件的对应关系

代码段(.text)

  • 源代码中的可执行语句编译后进入代码段
  • 代码段在有内存管理单元的系统中具有只读属性
  • 代码段的大小在编译结束后就已经固定(不能动态改变)
  • 代码段中可以包含常量数据(如:常量字符串)

数据段(.data,.bss,.rodata)

  • 数据段用于存放源代码中具有全局生命期的变量
    • .bss 存储未初始化(初始化为0)的变量
    • .data 存储具有非0初始值的变量
    • .rodata 存储const关键字修饰的变量

问题:同是全局变量和静态局部变量为什么初始化和未初始化的保存在不同段中?

深入理解.data和.bss

  • 程序加载后
    • .bss段中的所有内存单元被初始化为0
    • 将程序文件中的.data段相关的初始值写入对应内存单元

.bss段中的变量不用在程序文件中保存初始值,从而减小可执行程序文件的体积,并且提高程序的加载效率。

深入理解程序的结构

程序中的栈(Stack)

  • 程序中栈的本质是一片连续的内存空间
  • SP寄存器作为栈顶"指针"实现入栈操作和出栈操作

栈的深入理解

  • 中断发生时,栈用于保存寄存器的值
  • 函数调用时,栈用于保存函数的活动记录(栈帧信息)
  • 并发编程时,每一个线程拥有自己独立的栈

程序中的堆(Heap)

  • 堆是一片“闲置”的内存空间,用于提供动态内存分配
  • 堆空间的分配需要函数支持(malloc)
  • 堆空间在使用结束后需要归还(free)

内存映射段(Memory Mapping Segment)

  • 内核将硬盘文件的内容直接映射到内存映射段(mmap)
  • 动态链接库在可执行程序加载时映射到内存映射段
  • 程序执行时能够创建匿名映射区存放程序数据

内存映射文件原理简介

  • 将硬盘上的文件数据逻辑映射到内存中(零耗时)
  • 通过缺页中断进行文件数据的实际载入(一次数据拷贝)
  • 映射后的内存的读写就是对文件数据的读写

小结

14 缔造程序兼容的合约(上)

缔造程序兼容的合约

什么是ABI(Application Binary Interface)?

  • 应用程序二进制接口
    • 数据类型的大小、数据对齐的方式
    • 函数调用发生时的调用约定
    • 系统调用的编号,以及进行系统调用的方式
    • 目标文件的二进制格式、程序库格式、等等

什么是EABI(Embedded Application Binary Interface)?

  • 嵌入式应用程序二进制接口
    • 针对嵌入式平台的ABI规范
      • 可链接目标代码以及可执行文件格式的二进制规范
      • 编译链接工具的基础规范、函数调用规范、调试格式规范,等
      • EABI与ABI的主要区别是应用程序代码中允许使用特权指令

广义上ABI的概念

  • 泛指应用程序在二进制层面应该遵循的规范

狭义上ABI的概念

  • 特指
    • 某个具体硬件平台的ABI规范文档
    • 某个具体操作系统平台的ABI规范文档
    • 某个具体虚拟机平台的ABI规范文档

ABI规范示例

为什么下面的代码能够以0作为退出码结束程序运行?

asm volatile(
"movl $1,%eax\n" //#1→sys_exit
"movl $0,%ebx\n" //exit code
"int $0x80 \n"); //call sys_exit(0)

问题:ABI和API有什么不同?

  • ABI和API是不同层面的规范

    • ABI是二进制层面的规范
    • API是源代码层面的规范
  • ABI和API没有直接联系

    • 遵循相同ABI的系统,所提供的API可能不同
    • 所提供API相同的系统,遵循的ABI可能不同

实例分析:ABI实例分析

#include <stdio.h>

struct {
short s : 9;
int j : 9;
char c;
short t : 9;
short u : 9;
char d;
} s;

int main(int argc, char* argv[]) {
int i = 0;
int* p = (int*)&s;

printf("sizeof = %d\n", sizeof(s));

s.s = 0x1FF;
s.j = 0x1FF;
s.c = 0xFF;
s.t = 0x1FF;
s.u = 0x1FF;
s.d = 0xFF;

for(i=0; i<sizeof(s)/sizeof(*p); i++) {
printf("%X\n", *p++);
}


return 0;
}

缔造程序兼容的合约

ABI定义了基础数据类型的大小

  • x86平台ABI规范中的基础类型
-基础数据类型大小
Byte/UBytesigned/unsigned char1
Short/UShortsigned/unsigned short2
Int/UIntsigned/unsigned int4
Long/ULongsigned/unsigned long long8
Floatfloat4
Doubledouble8
Pointervoid*4

ABI vs 移植性

ABI定义了结构体/联合体的字节对齐方式

编程实验:位域的不同存储方式

#include <stdio.h>

struct {
short s : 9;
int j : 9;
char c;
short t : 9;
short u : 9;
char d;
} s;

int main(int argc, char* argv[]) {
int i = 0;
int* p = (int*)&s;

printf("sizeof = %d\n", sizeof(s));

s.s = 0x1FF;
s.j = 0x1FF;
s.c = 0xFF;
s.t = 0x1FF;
s.u = 0x1FF;
s.d = 0xFF;

for(i=0; i<sizeof(s)/sizeof(*p); i++)
{
printf("%X\n", *p++);
}


return 0;
}

缔造程序兼容的合约

ABI定义了硬件寄存器的使用方式

  • 寄存器是处理器用来数据和运行程序的重要载体
  • 一些寄存器在处理器设计时就规定好了功能
    • EIP(指令寄存器),指向处理器下一条要执行的指令
    • ESP(栈顶指针寄存器),指向当前栈存储区的顶部
    • EBP(栈帧基址存储器),指向函数栈帧的重要位置

x86寄存器的ABI规范示例

寄存器功能定义
EAX用于存放函数的返回值
EDX除法运算时需要使用这个寄存器
ECX计数器寄存器
EBX局部变量寄存器
ESI局部变量寄存器
EDI局部变量寄存器

PowerPC寄存器的ABI规范示例

寄存器类型功能定义
R0通用used to hold the old link register when build the stack frame
R1专用stack pointer
R2专用table of contents pointer
R3通用used as the return value of a function,and also the first argument in
R4-R10通用used to send in argument 2 through 8 into a function
R11-R12通用-
CR专用condition register

函数的调用约定

  • 当函数调用发生时
    • 参数会传递给被调用的函数
    • 而返回值会被返回给函数调用者
  • 调用约定描述参数如何传递到栈中以及栈的维护方式
    • 参数传递顺序(如:从右向左进行参数的入栈)
    • 调用栈清理(如:被调函数负责清理栈)

调用约定是ABI规范的一部分

调用约定通常用于库调用和库开发的时候

  • 从右到左依次入栈:__stdcall,__cdecl,__thiscall
  • 从左到右依次入栈:__pascal,__fastcall

小结

  • 广义上的ABI指应用程序在二进制层面需要遵循的约定
  • 狭义上的ABI指某一个具体硬件或者操作系统的规范文档
    • ABI定义了基础数据类型的大小
    • ABI定义了结构体/联合体的字节对齐方式
    • ABI定义了硬件寄存器的使用方式
    • ABI定义了函数调用时需要遵守的调用约定

15 缔造程序兼容的合约(下)

缔造程序兼容的合约

ABI定义了函数调用时

  • 栈帧的内存布局
  • 栈帧的形成方式
  • 栈帧的销毁方式

ebp是函数调用以及函数返回的核心寄存器

  • ebp为当前栈帧的基准(存储上一个栈帧的ebp值)
  • 通过ebp能够获取返回值地址,参数,局部变量,等

Linux中的栈帧布局

函数调用发生时的细节操作

  • 调用者通过call指令调用函数,将返回地址压入栈中
  • 函数所需要的栈空间大小由编译器确定,表现为字面常量
  • 函数结束时,leave指令恢复上一个栈帧的esp和ebp
  • 函数返回时,ret指令将返回地址恢复到eip(PC)寄存器

函数调用时的“前言”和“后续”

前言后续
push ebp move esp,ebp sub $(SIZE),esp push edi push esi push ebx push ...pop ... pop ebx pop esi pop edi leave ret

GDB小贴士:info frame命令输出的阅读

编程实验:函数栈帧结构初探

#include <stdio.h>

#define PRINT_STACK_FRAME_INFO() do \
{ \
char* ebp = NULL; \
char* esp = NULL; \
\
\
asm volatile ( \
"movl %%ebp, %0\n" \
"movl %%esp, %1\n" \
: "=r"(ebp), "=r"(esp) \
); \
\
printf("ebp = %p\n", ebp); \
printf("previous ebp = 0x%x\n", *((int*)ebp)); \
printf("return address = 0x%x\n", *((int*)(ebp + 4))); \
printf("previous esp = %p\n", ebp + 8); \
printf("esp = %p\n", esp); \
printf("&ebp = %p\n", &ebp); \
printf("&esp = %p\n", &esp); \
} while(0)

void test(int a, int b) {
int c = 3;

printf("test() : \n");

PRINT_STACK_FRAME_INFO();

printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
printf("&c = %p\n", &c);
}

void func() {
int a = 1;
int b = 2;

printf("func() : \n");

PRINT_STACK_FRAME_INFO();

printf("&a = %p\n", &a);
printf("&b = %p\n", &b);

test(a, b);
}

int main() {
printf("main() : \n");

PRINT_STACK_FRAME_INFO();

func();

return 0;
}

缔造程序兼容的合约

问题:函数调用时,参数如何入栈?函数返回时,返回值在哪里?

C语言默认使用的调用约定(__cdecl__)

  • 调用函数时,参数从右向左入栈
  • 函数返回时,函数的调用者负责将参数弹出栈
  • 函数返回值保存在eax寄存器中

其它各种调用约定

调用约定意义备注
_stdcall_1. 参数从右向左入栈 2. 被调函数清理栈中的参数 3. 返回值保存在eax寄存器中-
_fastcall_1. 使用ecx和edx传递前两个参数 2. 剩下的参数从右向左入栈 3. 被调函数清理栈中的参数 4. 返回值保存在eax寄存器中-
_thiscall_1. C++成员函数的调用约定 2. 参数从右至左的方式入栈 3. 返回值保存在eax寄存器中如果参数确定 - this指针存放于ecx寄存器 - 函数自身清理栈中的参宿 如果参数不确定 - this指针在所有参数入栈后再入栈 - 调用者清理栈中的参数

一些注意事项

  • 只有使用了__cdecl__的函数支持可变参数定义
  • 当类的成员函数为可变参数时,调用约定自动变为_cdecl_
  • 调用约定定义了函数被编译后对应的最终符号名

编程实验:函数调用约定

#include <stdio.h>

int test(int a, int b, int c)
{
return a + b + c;
}

void __attribute__((__cdecl__)) func_1(int i)
{
}

void __attribute__((__stdcall__)) func_2(int i)
{
}

void __attribute__((__fastcall__)) func_3(int i)
{
}

int main()
{
int r = test(1, 2, 3);

printf("r = %d\n", r);

return 0;
}

缔造程序兼容的合约

问题:当返回值类型为结构体时,如何将值返回到调用函数中?

  • 结构体类型的返回值

    • 函数调用时,接收返回值的变量地址需要入栈
    • 被调函数直接通过变量地址拷贝返回值
    • 函数返回值用于初始化与赋值对应的过程不同

函数返回值初始化变量

函数返回值给变量赋值

编程实验:结构体函数返回值

#include <stdio.h>

struct ST {
int x;
int y;
int z;
};

struct ST f(int x, int y, int z) {
struct ST st = {0};

printf("f() : &st = %p\n", &st);

st.x = x;
st.y = y;
st.z = z;

return st;
}

void g() {
struct ST st = {0};

printf("g() : &st = %p\n", &st);

st = f(1, 2, 3);

printf("g() : st.x = %d\n", st.x);
printf("g() : st.y = %d\n", st.y);
printf("g() : st.z = %d\n", st.z);
}

void h() {
struct ST st = f(4, 5, 6);

printf("h() : &st = %p\n", &st);
printf("h() : st.x = %d\n", st.x);
printf("h() : st.y = %d\n", st.y);
printf("h() : st.z = %d\n", st.z);
}

int main() {
h();
g();

return 0;
}

小结

  • 栈帧是函数调用时形成的链式内存结构
  • ebp是构成栈帧的核心基准寄存器
  • 调用约定决定了函数调用时的细节行为
  • 基础数据类型的返回值通过eax传递
  • 结构体类型的返回值通过内存拷贝完成