13、深入理解程序结构

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

程序的静态特征就是指令和数据。

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

源程序到可执行程序文件的对应关系:图13.1

初始化后去.data 未初始化去.bss

局部变量在栈上,可执行函数语句去代码段

大部分去了可执行程序。

代码段(.text):可执行不可写

源代码中的可执行语句编译后进入代码段。

代码段在有内存管理单元的系统中具有只读属性。预防恶意软件破坏

代码段的大小在编译结束后就已经固定(不能动态改变)。

代码段中可以包含常量数据(如:常量字符串)。

数据段(.data, .bss, .rodata) 全局变量和静态局部变量

数据段用于存放源代码中具有全局生命期的变量。

.bss:存储未初始化(初始化为0)的变量

.data:存储具有非0初始值的变量。

.rodata:存储const关键字修饰的变量。只读:readonly。用const修饰的具有全局生命期的变量。比如:const全局变量

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

深入理解 .data和 .bss: 效率:存储效率,加载效率

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

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

gcc -e dt_main test.c -nostartfiles -o test.out //-e 指明入口函数是de_main() -nostartfiles  不使用c库自带启动文件

char g_no_value;
int g_value=1;
int dt_main()
{
static char  c_no_value;
static int c_value=2;
return 0;
}

gcc编译器按4字节对齐方式。

函数编译之后进入代码段:

objdump -h test.out:5 .text         0000000a  08048190  08048190  00000190  2**2

nm test.out: 08048190 T dt_main

dt_main位于代码段,入口函数的地址就是objdump VMA的.text代码段的地址。

虽然在代码中初始化后与未初始化后的静态全局变量时挨着的,但是编译过后就不在一起了。

8 .bss    00000004  08049ffc  08049ffc  00000ffc  2**2

08049ffc  b  c_no_value.1249

==》.bss的地址是08049ffc。变量c_no_value的地址也是这个,所以它存储于 .bss段。

08049ffc b c_no_value.1249

08049ffd B g_no_value

==》c_no_value地址加1就是g_no_value的地址。

 objdump -s -j .data test.out  //查看.data数据段中保存的初始值信息
test.out:     file format elf32-i386
Contents of section .data:

 8049ff4(.data段起始地址) 01000000(存储四个字节的数据) 02000000        ........ 

 8049ff4这个地址对应的变量是g_value,它的初始值是01000000与程序对应

接下来存储的是02000000,而地址是 8049ff4+4=8049ff8

08049ff8 d c_value.1250,对应的是c_value,初始值是2.

意味着如果全局变量或静态局部变量有初始值在编译过后初始值就进入了最终可执行程序文件了。

添加:char g_str[]="d.t.software"; 全局的 13字节

 7 .data         00000018  08049ff4  08049ff4  00000ff4  2**2

.data增加了13; 所以成了8+13=21,四字节对齐就是24,上一行是16进制表示,所以18对应24是正确的。

delphi@delphi-vm:~/make$ nm test.out
08049f8c d _DYNAMIC
00000000 d _GLOBAL_OFFSET_TABLE_
0804a00c A __bss_start
0804a00c A _edata
0804a010 A _end
0804a00c b c_no_value.1250
0804a008 d c_value.1251
08048190 T dt_main
0804a00d B g_no_value
08049ff8 D g_str

08049ff4 D g_value

多了一个符号。

在增加一个const int g_c=3;

gcc -e dt_main test.c -nostartfiles -o test.out 

objdump -h test.out

多了一行:

6 .rodata       00000004  0804819c  0804819c  0000019c  2**2

然后;nm test.out

多了:

0804819c R g_c  内存地址与上边的.rodata起始地址是一样的。

说明const变量进入了rodata段。

查看.rodata段中记录的信息:objdump -s -j .rodata test.out

test.out:     file format elf32-i386
Contents of section .rodata:

804819c 03000000           

从地址804819c 开始记录的值03000000与const int g_c=3所一致。

3关键段的分析:

程序中的栈(Stack):

程序中栈的本质是一片连续的内存空间。

SP寄存器作为栈顶“指针”实现入栈操作和出栈操作。

在可执行程序加载到内存中建立栈。

程序中的栈和数据结构中的栈:程序中的栈是数据结构中栈的应用。

有数据进栈,sp向上移动,有出栈,sp向下移动。

从数据结构角度看:程序中的栈就是用顺序存储结构实现的数据结构。

栈的深入理解:

中断发生时,栈用于保存寄存器的值。

函数调用时,栈用于保存函数的活动记录(栈帧信息)。

并发编程时,每一个线程拥有自己独立的栈。多线程中每个线程都有一个栈

main函数隶属于程序中的一个线程,main函数隶属的线程是主线程,主线程有栈才能函数调用,其它的子线程也必须有栈才能多线程并发执行。

一个可执行程序是没法运行的,没有栈,入口函数都没法调用了。

程序中的堆(Heap):

堆是一片 “闲置”的内存空间,用于提供动态内存分配。

堆空间的分配需要函数支持(malloc)

堆空间在使用结束后需要归还(free)。

与栈一样,程序加载执行之后才建立。

所以静态工具是不能看到堆对应的段的。

内存映射段:(Memory Mapping Segment): 也是程序执行后建立的一片存储空间

内核将硬盘文件的内容直接映射到内存映射段(mmap)。常规

动态链接库在可执行程序加载时映射到内存映射段。

程序执行时能够在内存映射段创建匿名映射区存放程序数据。

内存映射文件原理简介:

将硬盘上的文件数据逻辑映射到内存中(零耗时)

通过缺页中断进行文件数据的实际载入(一次数据拷贝):第一次读

映射后的内存的读写就是对文件数据的读写。

缺页中断:在中断处理程序里将文件的实际数据载入到内存中。

nm --列出目标文件(.o)的符号清单

小结:图

14、缔造程序兼容的合约

什么是ABI(Application Binary Interface)?

应用程序二进制接口:相当于规范

数据类型的大小,数据对齐方式。

函数调用发生时的调用约定。关于函数调用时参数入栈的规范,返回值的规范等等

系统调用的编号,以及进行系统调用的方式。

目标文件的二进制格式,程序库格式、等等。

什么是EABI(Embedded Application Binary Interface)?

嵌入式应用程序二进制接口:

针对嵌入式平台的ABI规范

可链接目标代码以及可执行文件格式的二进制规范。

编译连接工具的基础规范、函数调用规范、调试格式规范等。

EABI与ABI的主要区别是EABI应用程序代码中允许使用CPU特权指令。只有这一个区别

广义上的ABI概念:

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

狭义上ABI的概念:

特指:

某个具体硬件平台的ABI规范文档。

某个具体操作系统平台的ABI规范文档。

某个具体虚拟机平台的ABI规范文档。

ABI规范示例:

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

asm volatile(

    "movl $1, %eax\n"  //#1 ->sys_exit ABI规定系统调用参数1是exit

    "movl $0, %ebx\n"  // exit code

    "int $0x80(中断)  \n") ; // call sys_exit(0)

规定了发生中断的的时候eax ,ebx的作用,应该怎样使用

ABI(应用程序二进制接口)和API(应用程序编程接口)有什么不同?

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

ABI是二进制层面的规范,API是源代码层面的规范。

ABI和API没有直接联系:

遵循相同API的系统,所提供的API可能不同。win和linux

所提供API相同的系统,遵循的ABI可能不同。qt在win和linux

ABI示例分析:跨平台程序原理

qt:直接将源代码编译成对应操作系统平台ABI规范的可执行程序。

java:将java可执行程序的ABI规范在不同的操作系统平台上进行实时转换。

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

                        基础数据类型                大小

Byte/UByte    signed/unsigned char    1

Short/UShort signed/unsigned short  2

Int/UInt        signed/unsigned int        4

Long/ULong    signed/unsigned int    8

Float                float                              4

Double            double                          8

Pointer            void*                             4

x86平台ABI规范中的基础类型

不同平台基础类型大小可能不同

ABI vs 移植性

应用程序逻辑层

应用程序框架层

类型适配层(类型及大小不随平台而改变) (平台改变时,只需要重定义类型适配层)

基础类型及大小(ABI规范定义) (平台不同时,基础类型及大小可能不同)

硬件体系架构(CPU)

屏蔽基础类型及大小ABI,在类型适配层创建我们自己的类型,这样我们只要根据硬件平台修改适配层就可以,上边的代码不用改。不随平台的改变而改变。

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

struct{     

    char c;     =>默认4字节对齐   s   c  字节0

    short s;

};

struct {

    char c;

    char d;  

    short s;   =>默认4字节对齐   s(2)  d  c (字节0) i (字节4)

    int i;

};

struct{

    char c;  (4个字节)

    double d; (8个字节)

    short s;  (4个字节)

};

union{   //联合体:所有成员共用一段内存,大小由最大成员决定,4个字节

    char c;  (第一个字节)

    short s; (前两个字节)

    int i;   (4个字节)

};

位域结构体:一个结构体成员使用的内存以比特来记不是字节来记。

struct{

    short s:9;  //9个比特

    int j    : 9;  //9个比特

    char c;   //8比特

    short t : 9 ;  //9个比特

    short u: 9; //9个比特

    char d; //8个比特

};

压缩存储:s跟j挨着存储,四字节对齐

非压缩存储:s占四个字节,使用了9位,j也占4字节,使用9位

#include <stdio.h>
struct {
    short s : 9;
    int j : 9;
    char c;
    short t : 9;
    short u : 9;
    char d;
} s; //定义了一个全局结构体s,未初始化都赋值为0,bss段
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;

}

打印:linux中

sizeof = 12
FF03FFFF  // 4个字节,s和j和c成员
1FF01FF   //t跟u

FF   //d

这里是压缩形式的对齐方式,linux中对位域结构体采用压缩形式的存储方式。

windows中:

sizeof =16

1FF    //s自己

1FF   //j自己

1FF00FF // c和t

FF01FF  //u,d

windows上是非压缩存储方式

不同操作系统ABI规范也是不一样的,win和linux就不一样。

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

寄存器是处理器用来处理数据和运行程序的重要载体。

一些寄存器在处理器设计时就规定好了功能:

EIP(指令寄存器),指向处理器下一条要执行的指令。

ESP(栈顶指针寄存器),指向当前栈存储区的顶部。

EBP(栈帧基址寄存器),指向函数栈帧的重要位置。

x86寄存器ABI规范示例:

寄存器        功能定义

EAX            用于存放函数的返回值

EDX            除法运算时需要使用这个寄存器

ECX            计数器寄存器

EBX            局部变量寄存器

ESI              局部变量寄存器

EDI             局部变量寄存器

PowerPC寄存器的ABI规范示例:

寄存器        类型   功能定义

R0            通用

R1            专用  stack pointer

函数调用约定:

当函数调用发生时:参数会传递给被调用的函数。而返回值会被返回给函数调用者。

调用约定描述参数如何传递到栈中以及栈的维护方式:参数传递顺序(如:从右向左进行参数的入栈)。调用栈清理(如:被调用函数负责清理栈)

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

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

从右到左一次入栈:_stdcall, _cdecl, _thiscall

从左到右一次入栈:_pascal, _fastcall

两种语言开发程序:

A编译器->主程序f(2,3); ->库文件procedure f(a:integer,b:integer);begin end;  <-B编译器

左边c语言 _cdecl,右边pscall语言_pascal

实例分析:

vc++ vs c++builder 图14.3

小结:

广义上的ABI指应用程序在二进制层面需要遵守的约定。

狭义上的ABI指某一个具体硬件或者操作系统的规范文档。

-ABI定义了基础类型的大小,定义了结构体/联合体的字节对齐方式,定义了硬件寄存器的使用方式,定义了函数调用时需要遵守的调用约定。

15、ABI

ABI定义了函数调用时:

栈帧的内存方式,栈帧的形成方式,栈帧的销毁方式

活动记录(栈帧)

参数

返回地址

old ebp -----ebp

寄存器信息

局部变量

其它数据信息----esp

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

ebp为当前栈帧的基准(存储上一个栈帧的ebp值)

通过ebp能够获取返回值地址,参数,局部变量,等

             栈帧偏移            意义

caller     ebp+4(n+1)    存储第n个参数的值   (调用者)

              ebp+8 (当前栈帧的开始)存储第1个参数的值(上一个esp指向的位置)(这里是函数调用前esp指向的位置)

callee     ebp+4            存储函数返回的地址   (被调用者)

              ebp                存储上一个栈帧的ebp值(上边调用者ebp)

             ebp-4            存储寄存器,局部变量,临时变量等


上边是高地址下边是低地址


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

调用者通过call指令调用函数,将返回地址压入栈中。

函数所需要的栈空间大小由编译器确定,表现为字面常量(汇编中)。

函数结束时,leave指令恢复上一个栈帧的esp和ebp

函数返回时,ret指令将返回地址恢复到eip(PC)寄存器。(指令寄存器)

leave<-->move ebp, esp(esp从低地址返回到高地址) pop ebp(恢复上一个栈帧ebp基准地址)

esp从低地址返回到高地址,pop弹出来的值是上一个栈帧的基准地址,pop到ebp寄存器中,ebp保存的值就是调用者栈帧的基准地址。

ret <-->pop eip

函数调用时的前言和后续:

前言                    后续

push ebp(将ebp的值压入栈中) pop(这颜色表示将当前寄存器保存在当前栈帧里边)

move esp,ebp    pop ebx

sub $(SIZE),esp(-常数值)  pup esi(这颜色表示相关寄存器值的恢复工作)

push edi (这颜色表示将当前寄存器保存在当前栈帧里边)   pup edi

push  esi            leave(摧毁当前栈帧)

push ebx            ret(函数返回)

push....

这里对于通用寄存器edi,esi,ebx的 push(pop)操作并不是压栈(出栈)操作,而是表示将对应寄存器的值存入栈帧中(从栈帧中取出),存取的顺序正好相反。

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;
}

shell gcc -g frame.c -o test.out

file test.out

start

break frame.c:35

info breakpoints

continue

main() : 
ebp = 0xbffff2b8
previous ebp = 0xbffff338
return address = 0x145ce7
previous esp = 0xbffff2c0
esp = 0xbffff290
&ebp = 0xbffff2ac
&esp = 0xbffff2a8
func() : 
ebp = 0xbffff288
previous ebp = 0xbffff2b8
return address = 0x80486d6
previous esp = 0xbffff290
esp = 0xbffff260
&ebp = 0xbffff274
&esp = 0xbffff270
&a = 0xbffff27c
&b = 0xbffff278
test() : 
ebp = 0xbffff258
previous ebp = 0xbffff288

return address = 0x8048601

previous esp = 0xbffff260
esp = 0xbffff230
&ebp = 0xbffff248
&esp = 0xbffff244
&a = 0xbffff260
&b = 0xbffff264

&c = 0xbffff24c //test的局部变量

证实:

ebp所指向的内存地址的地方存储了上一个栈帧的ebp基准地址。

(gdb) info frame
Stack level 0, frame at 0xbffff260:
 eip = 0x80484f7 in test (frame.c:27); saved eip 0x8048601
 called by frame at 0xbffff290
 source language c.
 Arglist at 0xbffff258, args: a=1, b=2
 Locals at 0xbffff258, Previous frame's sp is 0xbffff260
 Saved registers:

  ebp at 0xbffff258, eip at 0xbffff25c

当前栈帧从0xbffff260:开始,当前栈帧的开始在ebp+8偏移的地方。

返回地址在ebp+4的地方

(gdb) x /1wx 0xbffff25c

0xbffff25c: 0x08048601

上一个栈帧对应esp指针的值0xbffff260对应栈帧的开始在ebp+8偏移的地方。存储的值是1,参数

(gdb) x /1wx 0xbffff260

0xbffff260: 0x00000001

(gdb) x /1wx 0xbffff264

0xbffff264: 0x00000002

函数调用的参数在栈上

test(a,b) b先入栈,a后入栈,a对应的栈地址小。b对应的栈地址大

b入栈,然后a入栈,然后返回地址,然后ebp。图

 shell objdump -S test.out > test.s  //反汇编

   test(a, b); //函数调用  

 80485ef: 8b 55 f0        mov    -0x10(%ebp),%edx  // b->edx

 int b = 2;   //ebp向低地址偏移10个字节就是b
 8048506: c7 45 f0 02 00 00 00 movl   $0x2,-0x10(%ebp)

 80485f2: 8b 45 f4        mov    -0xc(%ebp),%eax  //a->eax

    int a = 1;
 80484ff: c7 45 f4 01 00 00 00 movl   $0x1,-0xc(%ebp)

 80485f5: 89 54 24 04  mov    %edx,0x4(%esp)

 80485f9: 89 04 24       mov    %eax,(%esp)

ba入栈

 80485fc: e8 f3 fd ff ff  call   80483f4 <test>

跳转到test函数所在地进行执行。

前言:func函数中

 80484f9: 55      push   %ebp //ebp入栈
 80484fa: 89 e5 mov    %esp,%ebp //ebp指向esp

 80484fc: 83 ec 28 sub    $0x28,%esp

 //-28常量是编译代码得到的,移动esp,换句话说当前func函数所对应的栈帧大小是0x28

080483f4 <test>:  //test 函数中
 80483f4: 55      push   %ebp
 80483f5: 89 e5  mov    %esp,%ebp

 80483f7: 83 ec 28  sub    $0x28,%esp  //

后续:

 8048601: c9                    leave  

 8048602: c3                    ret 

栈帧的形成及销毁:图

函数调用时,参数如何入栈?

函数返回时,返回值在哪里?

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

调用函数时,参数从右向左入栈。

函数返回时,函数的调用者负责将参数弹出栈。

函数返回值保存在eax寄存器中。

其它各种调用约定:

调用约定            意义            备注

_stdcall_    1.参数从右向左入栈。2被调函数清理栈中的参数3返回值保存在eax寄存器中

_fastcall_ 1使用ecx和edx传递前两个参数2剩下的参数从右向左入栈,3被调函数清理栈中的参数4返回值保存在eax寄存器

_thiscall_ 1c++成员函数的调用约定2参数从右向左方式入栈3返回值保存在eax寄存器中。如果参数确定:this指针存放于ECX寄存器,函数自身清理栈中的参数。如果参数不确定:this指针在所有参数入栈后再入栈,调用者清理栈中的参数,变为_cdecl_。

一些注意事项:

只有使用了_cdecl_的函数支持可变参数定义。

当类的成员函数为可变参数时,调用约定自动变为_cdelc.

调用约定定义了函数被编译后对应的最终符号名。

#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;

}

(gdb) info register

eax            0x6 6

objdump -S test.out >test.s

 804840c: e8 b3 ff ff ff        call   80483c4 <test>

 8048411: 89 44 24 1c          mov    %eax,0x1c(%esp) //r变量

(gdb) print /x &r

$1 = 0xbffff2ac

(gdb) info registers

esp            0xbffff290 0xbffff290

0xbffff290+1c=0xbffff2ac  //(赋值给r)

(gdb) info registers
eax            0xbffff364

(gdb) info registers

eax            0x6 6

确实是通过eax赋值的。

void __attribute__((__cdecl__)) func_1(int i)
{
 80483d5: 55                    push   %ebp
 80483d6: 89 e5                mov    %esp,%ebp
}
 80483d8: 5d                    pop    %ebp
 80483d9: c3                    ret    

080483da <func_2>:

void __attribute__((__stdcall__)) func_2(int i)
{
 80483da: 55                    push   %ebp
 80483db: 89 e5                mov    %esp,%ebp
}
 80483dd: 5d                    pop    %ebp
 80483de: c2 04 00              ret    $0x4

080483e1 <func_3>:

void __attribute__((__fastcall__)) func_3(int i)
{
 80483e1: 55                    push   %ebp
 80483e2: 89 e5                mov    %esp,%ebp
 80483e4: 83 ec 04              sub    $0x4,%esp
 80483e7: 89 4d fc              mov    %ecx,-0x4(%ebp)
}
 80483ea: c9                    leave  

 80483eb: c3                    ret    

前言和后续是不一样的,因为调用约定不一样。

说明:调用约定会决定函数调用细节行为。

nm test.out
080483d5 T func_1
080483da T func_2
080483e1 T func_3

 g++ -g test.cpp -o test.out
 nm test.out

080484a5 T _Z6func_1i
080484aa T _Z6func_2i
080484b1 T _Z6func_3i

变为cpp文件时,func函数的名字发生了变化。说明调用约定对最终编译生成的符号名产生影响。

开发共享库或动态链接库时,对符号名影响非常明显。演示动态链接库

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

结构体类型的返回值:

函数调用时,接收返回值的变量地址需要入栈。

被调函数直接通过变量地址拷贝返回值。(拷贝到内存)

函数返回值用于初始化与赋值对应的过程不同。

函数返回值初始化变量:

struct ST st= f(1,2,3); 将局部变量st的地址传入栈中

然后在f()函数里边通过栈这个地址将返回值直接拷贝到对应的内存空间中中。

函数返回值给变量赋值:

struct ST st={0};

st=f(4,5,6);

=>

struct ST st={0};

struct ST {temp}=f(4,5,6);

st={temp};

将临时变量temp的地址传入栈中。然后将返回值直接拷贝到对应的temp中。接下来用临时变量temp赋值给st。

#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;}

(gdb) backtrace
#0  f (x=4, y=5, z=6) at 3.c:21
#1  0x080484f2 in h () at 3.c:38

#2  0x08048552 in main () at 3.c:48

main调用h,h调用f

命令:frame 1  //切换到h的上下文

(gdb) info frame //打印当前上下文栈帧
Stack level 1, frame at 0xbffff2b0:
 eip = 0x80484f2 in h (3.c:38); saved eip 0x8048552
 called by frame at 0xbffff2c0, caller of frame at 0xbffff280
 source language c.
 Arglist at 0xbffff2a8, args: 
 Locals at 0xbffff2a8, Previous frame's sp is 0xbffff2b0
 Saved registers:

  ebp at 0xbffff2a8, eip at 0xbffff2ac

(gdb) print /x &st //打印st地址

$1 = 0xbffff294

frame 0 //切换回去

(gdb) info frame
 Arglist at 0xbffff278(ebp),

接下打印偏移处是不是有这个地址

(gdb) x /1wx 0xbffff280

0xbffff280: 0xbffff294

这个地址局部变量st的地址,在栈上,与课件一致。

然后反汇编查看:

objdump -S test.out > test.s

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

    lea    -0x14(%ebp),%eax //将 -0x14(%ebp)偏移地址放到eax,这个地址就是st的地址

mov    %eax,(%esp)  //eax入栈,也就是st地址

完了之后调用f 证毕

另一种情形:

(gdb) backtrace
#0  f (x=1, y=2, z=3) at 3.c:21
#1  0x08048476 in g () at 3.c:29
#2  0x08048557 in main () at 3.c:49

函数调用栈,main调用g,g调用f,在f中打印一下

(gdb) info frame
 eip = 0x804841a in f (3.c:21); saved eip 0x8048476

 Arglist at 0xbffff268, args: x=1, y=2, z=3

0xbffff268+8里面保存就应该是入栈的局部变量的地址。

(gdb) x /1wx 0xbffff270

0xbffff270: 0xbffff280

0xbffff280不是g中st的地址,(g() : &st = 0xbffff294)

所以是临时变量的地址。

反汇编:

lea    -0x28(%ebp),%eax
mov    %eax,(%esp)
mov    -0x28(%ebp),%eax
mov    %eax,-0x14(%ebp) (st)

同样也是将临时变量内存区域中的值拷贝到局部变量st里面。对应着逐个拷贝。

小结:

栈帧是函数调用时形成的链式内存结构。

ebp是构成栈帧的核心基准寄存器。

调用约定决定了函数调用时的细节行为。(最终符号名)

基础数据类型的返回值通过eax传递。

结构体类型的返回值通过内存拷贝完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值