Linux gcc 利用反汇编来研究C语言函数堆栈的分配方式

2023-05-16

    越来越感觉学习C和汇编才是最能接近计算机本质的途径。所以,今天开始研究汇编了,先从gcc反汇编开始。

    首先是下面的C代码:

#include <stdio.h>

int sum(int a,int b)
{
    char s=1;
    return a+b;
}

int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    sum(1,2);
    return 0;
}

在linux命令行下:

gcc -c b.c

objdump -d b.o > asm

即可得到C代码的反汇编形式,并保存在asm文件中,内容如下:


b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 10                 sub    $0x10,%esp
   6:    c6 45 ff 01              movb   $0x1,-0x1(%ebp)
   a:    8b 45 0c                 mov    0xc(%ebp),%eax
   d:    8b 55 08                 mov    0x8(%ebp),%edx
  10:    01 d0                    add    %edx,%eax
  12:    c9                       leave  
  13:    c3                       ret    

00000014 <main>:
  14:    55                       push   %ebp
  15:    89 e5                    mov    %esp,%ebp
  17:    83 ec 18                 sub    $0x18,%esp
  1a:    c7 45 f8 01 00 00 00     movl   $0x1,-0x8(%ebp)
  21:    c7 45 fc 02 00 00 00     movl   $0x2,-0x4(%ebp)
  28:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp)
  2f:    00
  30:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)
  37:    e8 fc ff ff ff           call   38 <main+0x24>
  3c:    b8 00 00 00 00           mov    $0x0,%eax
  41:    c9                       leave  
  42:    c3                       ret   
    1.%ebp---是帧寄存器,在函数中就是函数的基址寄存器,指向一个函数的栈底(帧底)。

    2.%esp---是栈寄存器,相当于是整个程序的基址寄存器,始终指向栈顶。

    3.push   %ebp 的意思是%ebp入栈,此时的%ebp保存的是上一个函数的帧起始地址,也即调用该函数的地址。 把%ebp压栈,保存起来,以便返回。

    4.mov    %esp,%ebp 的意思是  把%esp赋值给%ebp,%esp保存的是当前程序的栈顶,也即该函数所占用内存的起始地址。 把%esp赋值给%ebp,也就把%ebp设置成了当前函数的帧起始地址。

    5.sub    $***,%esp,并不会在每个程序中都会出现。可以尝试一下,如果一个函数没有任何局部变量,那么反汇编这句话也就 不会出来。这句话的意思是,把%esp减去一个数。我们知道栈空间是由高到底发展的,所以%esp++,相当于%esp=%esp-1。因为调用了新函数,而且该函数有局部变量,那么栈空间就变大了,所以要扩展栈空间,也即是修改%esp,让其指向更低的地址。而让%esp减去多少呢?这要看函数占用多少空间,于其中的局部变量有关,以及他将调用的函数参数有关。其并不计算其参数所占的空间,其参数所占的空间要算在调用它的函数中。
    6.call指令会将函数返回地址(call指令的下一条指令地址)入栈,并自动调整esp值,而ret指令则将栈顶值出栈,并作为函数返回地址,即装入eip,也自动调整esp,而leave指令,则相当于move esp,ebp;pop ebp;所以函数执行完毕之后,栈顶元素正确的指向调用函数之前压栈的eip函数返回地址。还有一点需要注意的是,call指令压栈的返回地址占用的空间大小。从被调用函数sum中对其参数的引用地址来看,貌似压入的是8个字节,但是感觉不合理,因为eip是4个字节啊,思考很久,才明白,是因为sum函数一开始进行了push ebp,保存了父函数的栈底地址,就这样又占用了4个字节,加上返回地址的4个字节,共8个字节。

    关于第5点,还需要详细说明一下,我做了好多实验,最终发现,sub    $***,%esp中,esp减去的数字,即扩展的栈空间的大小,等于本函数内部的有效局部变量的大小加上被调用函数的参数所占空间大小。具体来说,对于本函数内部的局部变量,栈空间的大小以16个字节为增长单位,即如果函数内的有效局部变量占用的空间不大于16个字节,就按照16个字节算,比如,若只有两个int变量,且一个int占4个字节,则为其分配16个字节,即sub    $0x10,%esp;若有4个int,同样是sub    $0x10,%esp;但若有5个int,或者4个int,一个char(一个char占一个字节),则sub    $0x20,%esp;而对于函数调用来说,就不一样了。函数调用的参数占用的栈空间以4字节为单位分配,即一个int就分配4个字节,两个int就分配8个字节,而一个char也分配4个字节(不足为奇,因为硬件就是以4个字节为单位压栈出栈的)。注意,对于本函数局部参数而言,两个int,三个char仍然占用16个字节。注意,上面所说的有效局部变量,是指在函数内被赋值的变量,若没赋值也没被使用,则不分配栈空间。

    但是,我发现一个奇怪的现象,即对于被调用函数的参数,若存在char类型的参数,则其栈空间。。。看如下代码:

#include <stdio.h>

int sum(int a,char b,char c)
{
   //a = 7;
   //b = 5;
   return 0;
}

int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    sum(1,2,3);
    return 0;
}

反汇编:


b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 08                 sub    $0x8,%esp
   6:    8b 55 0c                 mov    0xc(%ebp),%edx
   9:    8b 45 10                 mov    0x10(%ebp),%eax
   c:    88 55 fc                 mov    %dl,-0x4(%ebp)
   f:    88 45 f8                 mov    %al,-0x8(%ebp)
  12:    b8 00 00 00 00           mov    $0x0,%eax
  17:    c9                       leave  
  18:    c3                       ret    

00000019 <main>:
  19:    55                       push   %ebp
  1a:    89 e5                    mov    %esp,%ebp
  1c:    83 ec 1c                 sub    $0x1c,%esp
  1f:    c7 45 f8 01 00 00 00     movl   $0x1,-0x8(%ebp)
  26:    c7 45 fc 02 00 00 00     movl   $0x2,-0x4(%ebp)
  2d:    c7 44 24 08 03 00 00     movl   $0x3,0x8(%esp)
  34:    00
  35:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp)
  3c:    00
  3d:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)
  44:    e8 fc ff ff ff           call   45 <main+0x2c>
  49:    b8 00 00 00 00           mov    $0x0,%eax
  4e:    c9                       leave  
  4f:    c3                       ret    

    可见,在sum函数的汇编代码中,sub    $0x8,%esp,即又为两个char类型的参数分配了栈空间,并从其父函数(调用sum函数的函数main)的栈空间中将其值的低两个字节复制过来。why?另外,我注意到,若在sum函数中直接引用其参数a,b,int 类型参数a的重新赋值会直接引用其父函数中的栈空间,但char类型参数b不会直接引用其父函数栈中对应的参数空间,而是在sum函数中为其重新分配栈空间,why?如下:

#include <stdio.h>

int sum(int a,char b,char c)
{
   a = 7;
   b = 5;
   return 0;
}

int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    sum(1,2,3);
    return 0;
}

反汇编:


b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 18                 sub    $0x18,%esp
   6:    8b 55 0c                 mov    0xc(%ebp),%edx
   9:    8b 45 10                 mov    0x10(%ebp),%eax
   c:    88 55 ec                 mov    %dl,-0x14(%ebp)
   f:    88 45 e8                 mov    %al,-0x18(%ebp)
  12:    c7 45 fc 07 00 00 00     movl   $0x7,-0x4(%ebp)
  19:    c6 45 fb 05              movb   $0x5,-0x5(%ebp)
  1d:    b8 00 00 00 00           mov    $0x0,%eax
  22:    c9                       leave  
  23:    c3                       ret    

00000024 <main>:
  24:    55                       push   %ebp
  25:    89 e5                    mov    %esp,%ebp
  27:    83 ec 1c                 sub    $0x1c,%esp
  2a:    c7 45 f8 01 00 00 00     movl   $0x1,-0x8(%ebp)
  31:    c7 45 fc 02 00 00 00     movl   $0x2,-0x4(%ebp)
  38:    c7 44 24 08 03 00 00     movl   $0x3,0x8(%esp)
  3f:    00
  40:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp)
  47:    00
  48:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)
  4f:    e8 fc ff ff ff           call   50 <main+0x2c>
  54:    b8 00 00 00 00           mov    $0x0,%eax
  59:    c9                       leave  
  5a:    c3                       ret    

    从反汇编代码中可见, sub    $0x18,%esp/在sum中为a,b按照本地函数局部变量栈空间分配原则分配了16字节的栈空间,加上为两个char类型的参数所分配的栈空间,总共就是0x18,即24字节。
    但是随后的实验证明,如果在sum声明并定义局部变量而导致分配16字节的栈空间,则对int类型参数a的引用会直接引用父函数中的栈空间。
C代码:
#include <stdio.h>

int sum(int a,char b,char c)
{
   int s = a;
   a = 7;
  // b = 5;
   return 0;
}

int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    sum(1,2,3);
    return 0;
}
反汇编:

b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 18                 sub    $0x18,%esp
   6:    8b 55 0c                 mov    0xc(%ebp),%edx
   9:    8b 45 10                 mov    0x10(%ebp),%eax
   c:    88 55 ec                 mov    %dl,-0x14(%ebp)
   f:    88 45 e8                 mov    %al,-0x18(%ebp)
  12:    8b 45 08                 mov    0x8(%ebp),%eax
  15:    89 45 fc                 mov    %eax,-0x4(%ebp)
  18:    c7 45 08 07 00 00 00     movl   $0x7,0x8(%ebp)
  1f:    b8 00 00 00 00           mov    $0x0,%eax
  24:    c9                       leave  
  25:    c3                       ret    

00000026 <main>:
  26:    55                       push   %ebp
  27:    89 e5                    mov    %esp,%ebp
  29:    83 ec 1c                 sub    $0x1c,%esp
  2c:    c7 45 f8 01 00 00 00     movl   $0x1,-0x8(%ebp)
  33:    c7 45 fc 02 00 00 00     movl   $0x2,-0x4(%ebp)
  3a:    c7 44 24 08 03 00 00     movl   $0x3,0x8(%esp)
  41:    00
  42:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp)
  49:    00
  4a:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)
  51:    e8 fc ff ff ff           call   52 <main+0x2c>
  56:    b8 00 00 00 00           mov    $0x0,%eax
  5b:    c9                       leave  
  5c:    c3                       ret    
    但对char类型参数的引用仍会引用在本地函数内分配的新栈空间。why?
    最后要说明的一点是,对C语言中是要将for循环中的局部变量放循环内部声明还是放循环外部效率高的问题,见如下C代码和反汇编:
C:
#include <stdio.h>

int sum(int a,char b,char c)
{
  // int s = a;
  // a = 7;
  // b = 5;
   return 0;
}

int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    //sum(1,2,3);
    for (a=0;a<9;a++)
    {
    int x = 4;
    }
    int y = 5,yy=6;
    return 0;
}
反汇编:

b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 08                 sub    $0x8,%esp
   6:    8b 55 0c                 mov    0xc(%ebp),%edx
   9:    8b 45 10                 mov    0x10(%ebp),%eax
   c:    88 55 fc                 mov    %dl,-0x4(%ebp)
   f:    88 45 f8                 mov    %al,-0x8(%ebp)
  12:    b8 00 00 00 00           mov    $0x0,%eax
  17:    c9                       leave  
  18:    c3                       ret    

00000019 <main>:
  19:    55                       push   %ebp
  1a:    89 e5                    mov    %esp,%ebp
  1c:    83 ec 20                 sub    $0x20,%esp
  1f:    c7 45 ec 01 00 00 00     movl   $0x1,-0x14(%ebp)
  26:    c7 45 f0 02 00 00 00     movl   $0x2,-0x10(%ebp)
  2d:    c7 45 ec 00 00 00 00     movl   $0x0,-0x14(%ebp)
  34:    eb 0b                    jmp    41 <main+0x28>
  36:    c7 45 f4 04 00 00 00     movl   $0x4,-0xc(%ebp)
  3d:    83 45 ec 01              addl   $0x1,-0x14(%ebp)
  41:    83 7d ec 08              cmpl   $0x8,-0x14(%ebp)
  45:    7e ef                    jle    36 <main+0x1d>
  47:    c7 45 f8 05 00 00 00     movl   $0x5,-0x8(%ebp)
  4e:    c7 45 fc 06 00 00 00     movl   $0x6,-0x4(%ebp)
  55:    b8 00 00 00 00           mov    $0x0,%eax
  5a:    c9                       leave  
  5b:    c3                       ret    
    我得出的结论是:在没有优化的情况下(优化之后可能会将变量x放在寄存器中),无论是将x放在for循环内部还是外部,其实都是一样的,x都是放在栈空间中的,而且对其的引用都是直接引用其地址,而不是说,每声明定义一次都需要动态的“为其分配地址空间”,本质上,函数内部的所有局部变量,都在编译时期确定了栈空间大小和其在栈空间中的地址。所以效率上来说,for循环内的局部变量,无论是放在循环外部声明定义还是内部,应该都是一样的,即使存在硬件高速缓冲,我想,对此也没有影响。所以,是要将变量的声明定义放在循环内部还是外部,视代码的可读性而言的。我想,可能优化的时候,应该将循环内定义声明的变量,在执行完循环之后,重用其栈空间。但如上的反汇编代码所示(没有优化),编译器并没有重用x的栈空间给y或者yy。

如下实验:
C代码:
#include <stdio.h>

int sum(int a,char b,char c)
{
   return 0;
}

int sum1(int a,char b,char c,char g)
{
   return 0;
}

int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    sum(1,2,3);
    sum1(4,5,6,7);
    int y = 4,yy=6;
    return 0;
}
反汇编:

b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 08                 sub    $0x8,%esp
   6:    8b 55 0c                 mov    0xc(%ebp),%edx
   9:    8b 45 10                 mov    0x10(%ebp),%eax
   c:    88 55 fc                 mov    %dl,-0x4(%ebp)
   f:    88 45 f8                 mov    %al,-0x8(%ebp)
  12:    b8 00 00 00 00           mov    $0x0,%eax
  17:    c9                       leave  
  18:    c3                       ret    

00000019 <sum1>:
  19:    55                       push   %ebp
  1a:    89 e5                    mov    %esp,%ebp
  1c:    83 ec 0c                 sub    $0xc,%esp
  1f:    8b 4d 0c                 mov    0xc(%ebp),%ecx
  22:    8b 55 10                 mov    0x10(%ebp),%edx
  25:    8b 45 14                 mov    0x14(%ebp),%eax
  28:    88 4d fc                 mov    %cl,-0x4(%ebp)
  2b:    88 55 f8                 mov    %dl,-0x8(%ebp)
  2e:    88 45 f4                 mov    %al,-0xc(%ebp)
  31:    b8 00 00 00 00           mov    $0x0,%eax
  36:    c9                       leave  
  37:    c3                       ret    

00000038 <main>:
  38:    55                       push   %ebp
  39:    89 e5                    mov    %esp,%ebp
  3b:    83 ec 20                 sub    $0x20,%esp
  3e:    c7 45 f0 01 00 00 00     movl   $0x1,-0x10(%ebp)
  45:    c7 45 f4 02 00 00 00     movl   $0x2,-0xc(%ebp)
  4c:    c7 44 24 08 03 00 00     movl   $0x3,0x8(%esp)
  53:    00
  54:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp)
  5b:    00
  5c:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)
  63:    e8 fc ff ff ff           call   64 <main+0x2c>
  68:    c7 44 24 0c 07 00 00     movl   $0x7,0xc(%esp)
  6f:    00
  70:    c7 44 24 08 06 00 00     movl   $0x6,0x8(%esp)
  77:    00
  78:    c7 44 24 04 05 00 00     movl   $0x5,0x4(%esp)
  7f:    00
  80:    c7 04 24 04 00 00 00     movl   $0x4,(%esp)
  87:    e8 fc ff ff ff           call   88 <main+0x50>
  8c:    c7 45 f8 04 00 00 00     movl   $0x4,-0x8(%ebp)
  93:    c7 45 fc 06 00 00 00     movl   $0x6,-0x4(%ebp)
  9a:    b8 00 00 00 00           mov    $0x0,%eax
  9f:    c9                       leave  
  a0:    c3                       ret    
    可见,对于被调用函数的参数栈空间,是在父函数中分配的,按理说应该由被调用函数或者父函数在调用结束后清理,但是从汇编代码来看,并没有对其进行清理,而是不断由被调用函数重用。所以,函数内的栈空间分为两部分,一部分为本地局部变量分配空间,以16字节为单位;另一部分为被调用函数的参数栈空间分配,且以4字节为单位分配。而且,第二部分栈空间的大小以被调用的所有函数中,参数数量最多的,即占用参数栈空间最大的函数决定。所以,以上的main函数按照sum1函数的参数栈大小为其分配了16字节的栈空间,而sum函数参数栈空间占用12字节,但编译器并不为其单独分配12字节的空间,而是共用那16字节的参数栈空间。
如下实验:
C代码:
#include <stdio.h>

int sum(int a,char b,unsigned char c)
{
    printf("c=%d\n",c);
    return 0;
}


int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    long long c = -3;
    sum(1,2,c);
    return 0;
}
反汇编:

b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 18                 sub    $0x18,%esp
   6:    8b 55 0c                 mov    0xc(%ebp),%edx
   9:    8b 45 10                 mov    0x10(%ebp),%eax
   c:    88 55 f4                 mov    %dl,-0xc(%ebp)
   f:    88 45 f0                 mov    %al,-0x10(%ebp)
  12:    0f b6 45 f0              movzbl -0x10(%ebp),%eax
  16:    89 44 24 04              mov    %eax,0x4(%esp)
  1a:    c7 04 24 00 00 00 00     movl   $0x0,(%esp)
  21:    e8 fc ff ff ff           call   22 <sum+0x22>
  26:    b8 00 00 00 00           mov    $0x0,%eax
  2b:    c9                       leave  
  2c:    c3                       ret    

0000002d <main>:
  2d:    55                       push   %ebp
  2e:    89 e5                    mov    %esp,%ebp
  30:    83 e4 f0                 and    $0xfffffff0,%esp
  33:    83 ec 20                 sub    $0x20,%esp
  36:    c7 44 24 10 01 00 00     movl   $0x1,0x10(%esp)
  3d:    00 11111111
  3e:    c7 44 24 14 02 00 00     movl   $0x2,0x14(%esp)
  45:    00
  46:    c7 44 24 18 fd ff ff     movl   $0xfffffffd,0x18(%esp)
  4d:    ff
  4e:    c7 44 24 1c ff ff ff     movl   $0xffffffff,0x1c(%esp)
  55:    ff
  56:    8b 44 24 18              mov    0x18(%esp),%eax
  5a:    0f b6 c0                 movzbl %al,%eax
  5d:    89 44 24 08              mov    %eax,0x8(%esp)
  61:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp)
  68:    00
  69:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)
  70:    e8 fc ff ff ff           call   71 <main+0x44>
  75:    b8 00 00 00 00           mov    $0x0,%eax
  7a:    c9                       leave  
  7b:    c3                       ret    
    printf打印的c值为253。为啥?因为调用函数的时候,传递的实参c为long long类型(signed),占8个字节46到4e行是-3的补码形式,即计算机内部是以补码形式存放数值的。编译器使用movzbl指令将c参数入栈,因为sum函数声明中c的类型为unsigned char类型,所以先将long long类型强制类型转换,即取其低位一个字节并填充高三个字节为0入栈4个字节(使用movzbl,将其高三个字节填充为0,而低一个字节取long long的低一个字节)。因为c为-3,所以入栈的四个字节为0x000000fd(补码形式),sum中取其fd即11111101,也即253。这里我想表达的是,计算机内部使用的是补码,额。。。。。。
最后一个实验:
C代码:
#include <stdio.h>

char sum(int a,char b,char c)
{
    char s = b+c;
    return s;
}


int main(void)
{
    int a,b;
    a = 1;
    b = 2;
    sum(1,2,a);
    return 0;
}
反汇编:

b.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <sum>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    83 ec 18                 sub    $0x18,%esp
   6:    8b 55 0c                 mov    0xc(%ebp),%edx
   9:    8b 45 10                 mov    0x10(%ebp),%eax
   c:    88 55 ec                 mov    %dl,-0x14(%ebp) #这里是将父函数中的char类型的实参又复制了一份到本地函数栈中
   f:    88 45 e8                 mov    %al,-0x18(%ebp) #同上
  12:    0f b6 55 ec              movzbl -0x14(%ebp),%edx
  16:    0f b6 45 e8              movzbl -0x18(%ebp),%eax
  1a:    01 d0                    add    %edx,%eax #将两个char类型的变量当作两个int类型的变量来参与计算,即“类型提升”
  1c:    88 45 ff                 mov    %al,-0x1(%ebp)
  1f:    0f b6 45 ff              movzbl -0x1(%ebp),%eax
  23:    c9                       leave  
  24:    c3                       ret    

00000025 <main>:
  25:    55                       push   %ebp
  26:    89 e5                    mov    %esp,%ebp
  28:    83 ec 1c                 sub    $0x1c,%esp
  2b:    c7 45 f8 01 00 00 00     movl   $0x1,-0x8(%ebp)
  32:    c7 45 fc 02 00 00 00     movl   $0x2,-0x4(%ebp)
  39:    8b 45 f8                 mov    -0x8(%ebp),%eax
  3c:    0f be c0                 movsbl %al,%eax #相当于强制类型转换,将int类型转换为char类型,并置于一个4字节的内存单元中入栈(a)
  3f:    89 44 24 08              mov    %eax,0x8(%esp)
  43:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp) #直接将2作为4字节的int入栈(类型提升)
  4a:    00
  4b:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)
  52:    e8 fc ff ff ff           call   53 <main+0x2e>
  57:    b8 00 00 00 00           mov    $0x0,%eax
  5c:    c9                       leave  
  5d:    c3                       ret   
    关于类型提升:
    1.在32bit系统上,函数参数不管是char,short,int都是以4byte压入栈中。被调函数再根据定义,把实参裁减为定义的类型。从这句话来看,被调函数内部确实有必要将父函数中的实参“裁减”并复制到本地参数堆栈中。因为在C语言中,父函数所在的文件中可能没有被调函数的声明,所以这种情况下在父函数中并不会对入栈的参数进行“裁减”。
    2.整型提升就是char,short(无论unsigned,signed),位段类型,枚举类型都将提升为int类型。前提是int类型能完整容纳原先的数据,否则提升为unsigned int类型。
    3.类型提升时,高位填充的规则,若提升的目的类型为unsigned,则填充0,使用movzbl汇编指令;若提升的目的类型为signed类型,则填充符号位。
一个简单的例子:(来自http://blog.chinaunix.net/uid-23629988-id-292647.html)
    /***************************************************************/
    int main()
    {
    int i;
    unsigned char *p;
    char *p1;
    int a[] = {0xffffffff, 0xffffffff, 0xffffffff};
    p = a;
    p1 = a;
    for(i = 0 ; i < 8 ; i++) {
    printf(" 0x%02x  0x%02x \n", p[i], p1[i]);
    }
    }
    $ gcc main.c
    main.c: In function ‘main’:
    main.c:10: warning: assignment from incompatible pointer type
    main.c:11: warning: assignment from incompatible pointer type
    $ ./a.out
     0xff  0xffffffff
     0xff  0xffffffff
     0xff  0xffffffff
     0xff  0xffffffff
     0xff  0xffffffff
     0xff  0xffffffff
     0xff  0xffffffff
     0xff  0xffffffff
    。。。。。。 。。。。。。
    /***************************************************************/

根本原因其实很简单。
%x是打印无符号整数的16进制,而例子中传递的类型是字符型,那么这里就有一个字符提升的问题,将类型提升为无符号整形。
*p是unsigned char,其值为0xff,那么对应的无符号整形的值仍然是0xff。
而*p1确实char,其值为0xff,其对应的无符号整形的值为0xffffffff。为什么这次是0xffffffff呢?
因为*p1为-1,而无符号整数的-1则是0xffffffff。

为什么是这样呢?
因为在在编码为补码的情形下,类型提升有两种情况:
1. 符号扩展:对于有符号数,扩展存储位数的方法。在新的高位字节使用当前最高有效位即符号位的值进行填充。
2. 零扩展:对于无符号数,扩展存储位数的方法。在新的高位直接填0.

对于这个例子来说。*p是无符号数,所以填充的是0,即为0x000000ff。而*p1是有符号数,所以填充的是1,即为0xffffffff。

因此,从char型到unsigned int,是对有符号数的提升,因此用的是符号扩展,oxff被扩展为oxffffffff;而从unsigned char型到unsigned int型,是对无符号数的扩展,使用零扩展,oxff被扩展为ox000000ff,而填充的这些零是不会被打印出来的。

如果说这样教科书式的概念不容易理解。还有这样一种理解方式,也许不一定准确,但更容易理解。
对于这里的类型提升,整个步骤可以这样理解:
1. %x要求参数为无符号整数,需要参数为4个字节;
2. *p, *p1为(unsigned) char型,只占1个字节;
3. 因为参数的类型不符,需要扩展;
4. 定位需要扩展到4个字节;
5. 那么就需要填充增加的3个字节;
6. 这3个字节需要什么值?这里就需要上面所需要的概念了。针对有符号数和无符号数,进行不同值的填充。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux gcc 利用反汇编来研究C语言函数堆栈的分配方式 的相关文章

随机推荐

  • 深入理解Linux内核--信号

    信号用于在用户态进程间通信 内核也用信号通知进程系统所发生的事情 1 信号的作用 信号 signal 是很短的消息 xff0c 可以被发送到一个进程或一组进程 发送给进程的唯一信息通常是一个数 xff0c 以此来标识信号 使用信号的两个主要
  • 进程上下文与中断上下文

    处理器总处于以下状态中的一种 xff1a xff11 内核 态 xff0c 运行于进程 上下文 xff0c 内核代表进程运行于内核空间 xff12 内核态 xff0c 运行于中断上下文 xff0c 内核代表硬件运行于内核空间 xff13 用
  • 进程上下文VS中断上下文

    内核空间和用户空间是现代操作 系统的两种工作模式 xff0c 内核模块运行在内核空间 xff0c 而用户态应用程序运行在用户空间 它们代表不同的级别 xff0c 而对系统资源具有不同的访问权限 内核模块运行在最高级别 xff08 内核态 x
  • 中断不可睡眠的一些理解

    一 LINUX中到是有中断还没有完全返回就调用schedule 而睡眠过去的例子 可以猜是哪里 我觉得 xff0c 中断和异常不同 xff0c 中断是异步的 xff0c 异常和系统调用是同步的 异常比如缺页异常发生时 xff0c 当前任务在
  • kmalloc和vmalloc

  • linux内核中内存相关的操作函数

    1 kmalloc kfree static always inline void kmalloc size t size gfp t flags 内核空间申请指定大小的内存区域 xff0c 返回内核空间虚拟地址 在函数实现中 xff0c
  • Oracle安全:SCN可能最大值与耗尽问题Oracle安全:SCN可能最大值与耗尽问题

    SCN的问题一旦出现 xff0c 使得数据库的一切事务停止 xff0c 由于SCN不能后退 xff0c 所以数据库必须重建 xff0c 才能够重用 在2012年第一季度的CPU补丁中 xff0c 包含了一个关于SCN修正的重要变更 xff0
  • Linux内核 申请和释放内存流程

    1 内核初始化 xff1a 内核建立好内核页目录页表数据库 xff0c 假设物理内存大小为len xff0c 则建立了 3G 3G 43 len 0 len 这样的虚地址vaddr和物理地址paddr的线性对应关系 xff1b 内核建立一个
  • 编译器"自举与移植"原理

    本文基于对 编译原理与实践 中有关编译器自举与移植部分的读书 笔记 形式 xff0c 因为原书是老外写的 xff0c 感觉翻译的地方好多语句不通或难以理解 xff0c 所以花了好多功夫研究这一块 注 xff1a 本文中与原书一致的地方都是P
  • Linux 内核 由block_read和block_write函数引发的设备块号转换问题的思考

    在1 2内核版本中 xff0c 在Linux fs目录下 xff0c 有一个block dev c文件 xff0c 里面主要包含了block read block write block fsync函数 先说说我遇到的问题 xff0c 在块
  • Linux 进程调度时机

    Linux调度时机主要 有 xff1a 1 进程状态转换的时刻 xff1a 进程终止 进程睡眠 2 当前进程的时间 片用完时 xff08 current gt counter 61 0 xff09 3 设备驱动程序 4 进程从中断 异常及系
  • linux 下批量转换pdf的命令方法

    由于在windows下的图形界面 xff0c 难以批量进行其他格式的文件到PDF格式文件的转换 xff0c 而一些其他的软件也不是很满意 xff0c 所以转到linux下 xff0c 想利用linux强大的命令行来完成这件事 linux下有
  • Linux内核 内存映射文件机制mmap

    今天研究Linux1 2内核运行加载a out格式的可执行文件的代码时 xff0c 无意中研究明白了内核提供的内存映射机制 mmap xff08 memory map xff09 当内核要加载可执行文件到相应的用户地址空间时 xff0c 有
  • bash提示符的配置:

    bash提示符的配置 xff1a 如果您很容易使 shell 提示行变得色彩绚烂斓且带有更多信息 xff0c 为什么还要坚持用单调的标准 shell 提示行呢 xff1f 在这篇技巧中 xff0c Daniel Robbins 将说明如何获
  • Linux线性地址空间的划分及内核寻址方式

    今天研究Linux1 2内核时 xff0c 注意到该版本中的PAGE OFFSET宏被定义为0 xff0c 考虑到进程的地址空间被划分为3G的用户态地址空间和1G的内核态地址空间 xff0c 于是深入的研究了一下这个问题 一开始我只是疑惑
  • linux 最简单的模块的编写和运行

    第一次动手编写一个内核模块 xff0c 但是查找了许多资料没有一个可以完美通过编译的 xff0c 郁闷 xff0c 最后还是解决了 xff0c 分享出来 首先是hello c include lt linux kernel h gt Nee
  • 截获或替换linux系统调用

    直接上代码吧 xff1a hello c include lt linux kernel h gt Needed by all modules include lt linux module h gt Needed for KERN inc
  • oracle临时表实际应用

    xff08 这段是后面添加的 xff1a 临时表 xff0c 在实际应用中 xff0c 其实和nologging的固定表 xff0c 是差不多的 xff0c 都是中间表 xff0c 所以这里为什么添加这段话 xff0c 是让自己记得 xff
  • linux sys_call_table 初始化

    前几天看内核中系统调用代码 xff0c 在系统调用向量表初始化中 xff0c 有下面这段代码写的让我有点摸不着头脑 xff1a const sys call ptr t sys call table NR syscall max 43 1
  • Linux gcc 利用反汇编来研究C语言函数堆栈的分配方式

    越来越感觉学习C和汇编才是最能接近计算机本质的途径 所以 xff0c 今天开始研究汇编了 xff0c 先从gcc反汇编开始 首先是下面的C代码 xff1a include lt stdio h gt int sum int a int b