文章目录

  • 1.内存的平坦模型和分段模型
  • (1)平坦模型
  • (2)分段模型
  • 2.字节序,数据类型的高位与低位,寄存器
  • 3.栈
  • (1)函数调用和栈的关系
  • (2)gdb的backtrace(bt)使用

1.内存的平坦模型和分段模型

  • CPU通过内存总线访问到的地址称为物理地址
  • 32位下的最大物理地址是64GB,2^36次方;
  • 64位下,Intel的最大物理地址是2^40次方的字节,AMD是 2 ^48次方字节

(1)平坦模型

  • LInux采用的内存模型: 内存可看做是单一,平坦的连续地址空间,称之为线性地址空间

(四)GDBdebug调试技术——必须知道的栈知识_寄存器

(2)分段模型

  • 将内存看做被称之为“段segment”的独立地址空间的集合。
  • 通过段选择器和偏移量组成的逻辑地址来访问段内地址。首先用段选择器识别:要访问的段,然后通过偏移量找到该段的地址空间的内存。
  • 32bit下最多能指定16383个段,各段的最大大小位2^32次方字节
  • 64bit模式下,使用了平坦模型,因此可以使用64bit的线性地址。不能使用分段式的内存模型。

2.字节序,数据类型的高位与低位,寄存器

(1)大端字节选和小端字节序

例如:0x12345678
小端字节序:低位数据排在内存低地址,Intel架构
大端字节序:低位数据放在内存的高地址,SPARC,MIPS架构
  • Intel的CPU构造
  • 小端字节序

(3)32bit的寄存器与64bit的寄存器

  • 32bit的主要寄存器用途
  • (四)GDBdebug调试技术——必须知道的栈知识_寄存器_02

  • 32bit的主要段寄存器的用途
程序代码放在代码段;
数据放在数据段;
程序的栈放在堆栈段;

(四)GDBdebug调试技术——必须知道的栈知识_python_03

  • 64bit环境中的寄存器
64bit模式下的通用寄存器在处理32bit操作数时,可通过EAX,EBX,ECX,EDX,EDI,ESI,EBP,ESP,R8D-R15D;
64bit模式下的通用寄存器在处理64bit操作数时,可通过RAX,RBX,RCX,RDX,RDI,RSI,RBP,RSP,R8-R15;

R8-R15/R8D-R15D是8个新的通用寄存器,
RIP寄存器是64bit的指令指针;

3.栈

  • 栈stack是程序存放数据的内存区域之一,特点是LIFO。在保存动态分配的局部变量时,要使用栈。此外,在函数调用时,栈还用于传递函数参数,以及用于保存返回地址和返回值。

(1)函数调用和栈的关系

#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
#define MAX (1UL<<20)

/*
王继编写
GB2312
*/
typedef unsigned long long u64;
typedef unsigned int u32;

u32 max_addend=MAX;

u64 sum_till_MAX(u32 n)
{
    u64 sum;
    n++;
    sum=n;

    if (n<max_addend)
        sum+=sum_till_MAX(n);
    return sum;
}

int main(int argc, char **argv)
{
    u64 sum=0;
    if ((argc==2)&& isdigit(*(argv[1])))
        max_addend=strtol(argv[1], NULL, 0);
    if (max_addend>MAX || max_addend==0)
        {
            fprintf(stderr,"Invalid number is specified\n");
            return 1;
        }
    
    sum=sum_till_MAX(0);
    printf("sum(0..%lu)=%llu\n", max_addend, sum);
    return 0;
}
  • 执行结果如下:
[root@localhost wangji]# gcc stack.c -o sum
[root@localhost wangji]# ./sum 10
sum(0..10)=55
  • (a)是函数调用之前栈的状态
  • (b)是调用sum_till_MAX()函数之后栈的状态
  • (c)是再次调用sum_till_MAX函数之后,栈的状态
栈上依此(从下至上)保存了传给参数的函数,调用者的返回地址,上层栈帧指针和函数内部使用的自动变量;
每个函数独自拥有:用栈来临时保存寄存器,称之为栈帧stack frame;
帧指针FP:栈帧起始地址;
栈指针SP:永远指向栈的顶端;

(gdb) disassemble main
   0x00000000004006eb <+178>:   mov    $0x0,%edi             (1)
   0x00000000004006f0 <+183>:   callq  0x400604 <sum_till_MAX>   (2)
   0x00000000004006f5 <+188>:   mov    %rax,-0x8(%rbp)
   0x00000000004006f9 <+192>:   mov    0x20041d(%rip),%ecx        # 0x600b1c <max_addend>

(1)调用函数时,首先将传递给函数的参数压入栈中
(2)sum_till_MAX()函数的call指令自动把返回地址 0x00000000004006f5压入栈中



(gdb) disass sum_till_MAX
Dump of assembler code for function sum_till_MAX:
   0x0000000000400604 <+0>:     push   %rbp                   (3)
   0x0000000000400605 <+1>:     mov    %rsp,%rbp				(4)
   0x0000000000400608 <+4>:     sub    $0x20,%rsp				(5)
   0x000000000040060c <+8>:     mov    %edi,-0x14(%rbp)			(6)
   0x000000000040060f <+11>:    addl   $0x1,-0x14(%rbp)
   0x0000000000400613 <+15>:    mov    -0x14(%rbp),%eax
   0x0000000000400616 <+18>:    mov    %rax,-0x8(%rbp)			(7)
   0x000000000040061a <+22>:    mov    0x2004fc(%rip),%eax        # 0x600b1c <max_addend>
   0x0000000000400620 <+28>:    cmp    %eax,-0x14(%rbp)			
   0x0000000000400623 <+31>:    jae    0x400633 <sum_till_MAX+47>
   0x0000000000400625 <+33>:    mov    -0x14(%rbp),%eax
   0x0000000000400628 <+36>:    mov    %eax,%edi
   0x000000000040062a <+38>:    callq  0x400604 <sum_till_MAX>
   0x000000000040062f <+43>:    add    %rax,-0x8(%rbp)
   0x0000000000400633 <+47>:    mov    -0x8(%rbp),%rax
   0x0000000000400637 <+51>:    leaveq						(8)
   0x0000000000400638 <+52>:    retq						(9)
End of assembler dump.

上图(a)(b)(c)中的栈帧准备过程:
(3)在栈上能够,保存上层帧的帧指针FP
(4)将新的栈帧赋给帧指针FP
(5)在栈上分配用于,保存局部变量的空间

(6)开始为sum_till_MAX()函数的处理过程
(7),由于sum是64bit的,这里表示sum的低32bit,%rax中保存的是参数n的值,这里相当于sum=n;

(8)删除栈帧
(9)子程序的返回指令,将栈中保存的返回地址pop到程序计数寄存器中,将控制权返回给调用者

(2)gdb的backtrace(bt)使用

1.首先在这行代码打个断点:sum+=sum_till_MAX(n);
(gdb) bt
#0  sum_till_MAX (n=2) at stack.c:22
#1  0x000000000040062f in sum_till_MAX (n=1) at stack.c:22
#2  0x00000000004006f5 in main (argc=1, argv=0x7fffffffe488) at stack.c:37

2.下面来手动执行一下与gdb的backstrace相同的操作:
当前执行位置可以通过程序计数器pc获取,32bit的机器是eip,64bit的机器是rip!!;
帧指针FP:32bit的机器是ebp,64bit的机器是rbp;
(gdb)i r
rax            0x100000 1048576
rbx            0x0      0
rcx            0x31689894e8     212208227560
rdx            0x7fffffffe498   140737488348312
rsi            0x7fffffffe488   140737488348296
rdi            0x1      1
rbp            0x7fffffffe340   0x7fffffffe340<-帧指针FP
rsp            0x7fffffffe320   0x7fffffffe320
r8             0x400720 4196128
r9             0x316820e1b0     212200382896
r10            0x7fffffffe200   140737488347648
r11            0x3168621150     212204654928
r12            0x400520 4195616
r13            0x7fffffffe480   140737488348288
r14            0x0      0
r15            0x0      0
rip            0x400625 0x400625 <sum_till_MAX+33>
eflags         0x283    [ CF SF IF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0


3.查看栈的内容:从栈顶sp开始显示适当的大小
 32bit的机子上使用:
(gdb)x/40w $sp                                    

64bit的机子上使用:
(gdb)x/40g $sp  

可以结合上面的图更好的去理解,下面的字节。因为形参n=2(从bt指令可以看出),所以才有栈帧B,C;
如果形参n=1,那么只有栈帧B。
下面的,还是在#0  sum_till_MAX (n=2) at stack.c:22 基础上执行的。

0x7fffffffe320: 0x0000000000f0b2e4      0x00000002000000c2--------------------------
0x7fffffffe330: 0x0000000000000001      0x0000000000000002<-参数n		 [栈帧C]
0x7fffffffe340:<-帧指针FP 0x00007fffffffe370 <-返回地址     0x000000000040062f    帧指针FP就是上面(gdb) i b所得到的rbp的位置
0x7fffffffe350: 0x0000000000000000      0x00000001004004b3 -------------------------
0x7fffffffe360: 0x00007fffffffe498      0x0000000000000001<-参数n			[栈帧B]
0x7fffffffe370: <-帧指针FP 0x00007fffffffe3a0 <-返回地址      0x00000000004006f5
0x7fffffffe380: 0x00007fffffffe488      0x0000000100400520--------------------------
0x7fffffffe390: 0x00007fffffffe480      0x0000000000000000
0x7fffffffe3a0: 0x0000000000000000      0x000000316862123d
0x7fffffffe3b0: 0x0000000000000000      0x00007fffffffe488        		 
0x7fffffffe3c0: 0x0000000100000000      0x0000000000400639
0x7fffffffe3d0: 0x0000000000000000      0xb92bcbaf5ab5f584
0x7fffffffe3e0: 0x0000000000400520      0x00007fffffffe480
0x7fffffffe3f0: 0x0000000000000000      0x0000000000000000			[栈帧A]
0x7fffffffe400: 0x46d434509dd5f584      0xb9491b6b7945f584
0x7fffffffe410: 0x00007fff00000000      0x0000000000000000
0x7fffffffe420: 0x0000000000000000      0x0000000000400730  <-返回地址
0x7fffffffe430: 0x00007fffffffe488 <-参数argv   0x0000000000000001 <-参数argc-------------
0x7fffffffe440: 0x0000000000000000      0x0000000000000000
0x7fffffffe450: 0x0000000000400520      0x00007fffffffe480

3.使用gdb操作栈帧
(gdb) bt

查看现在选择的帧
(gdb)frame

选择上一层的#1帧
(gdb) frame 1

info指令可以看到更详细的栈帧信息
(下面的,还是在#0  sum_till_MAX (n=2) at stack.c:22 基础上执行的。)
(gdb)i frame 1
Stack frame at 0x7fffffffe380:
 rip = 0x40062f in sum_till_MAX (stack.c:22); saved rip 0x4006f5
 called by frame at 0x7fffffffe3b0, caller of frame at 0x7fffffffe350
 source language c.
 Arglist at 0x7fffffffe370, args: n=1
 Locals at 0x7fffffffe370, Previous frame's sp is 0x7fffffffe380
 Saved registers:
  rbp at 0x7fffffffe370, rip at 0x7fffffffe378


4.栈溢出stack overflow怎么看?
$ ./sum
Segmentation falut
(gdb) r
Program received signal SIGSEGV, Segmentation fault.
0x000000000040060c in sum_till_MAX (n=Cannot access memory at address 0x7fffff3fefec
) at stack.c:16
16      {

将sum_till_MAX()的参数n push到栈顶端的命令:
(gdb) x/i $pc  (关于内存地址的都用x吧)            等于p $pc ,与(gdb) i reg rip的内容一样
0x40060c 0x40060c <sum_till_MAX+8>

栈指针SP的位置
(gdb) p $sp        等于x/i $pc(关于内存地址的都用x吧) 和(gdb)x/i $rsp 结果一样
$2 = (void *) 0x7fffff3fefe0

查看内存映射
(gdb) i proc mapping
process 25903
cmdline = '/home/wangji/sum'
cwd = '/home/wangji'
exe = '/home/wangji/sum'
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000          0                             /home/wangji/sum
            0x600000           0x601000     0x1000          0                             /home/wangji/sum
        0x3168200000       0x3168220000    0x20000          0                         /lib64/ld-2.14.1.so
        0x3168420000       0x3168421000     0x1000    0x20000                         /lib64/ld-2.14.1.so
        0x3168421000       0x3168422000     0x1000    0x21000                         /lib64/ld-2.14.1.so
        0x3168422000       0x3168423000     0x1000          0
        0x3168600000       0x3168785000   0x185000          0                         /lib64/libc-2.14.1.so
        0x3168785000       0x3168985000   0x200000   0x185000                         /lib64/libc-2.14.1.so
        0x3168985000       0x3168989000     0x4000   0x185000                         /lib64/libc-2.14.1.so
        0x3168989000       0x316898a000     0x1000   0x189000                         /lib64/libc-2.14.1.so
        0x316898a000       0x316898f000     0x5000          0
      0x7ffff7fdd000     0x7ffff7fe0000     0x3000          0
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000          0
      0x7ffff7ffe000     0x7ffff7fff000     0x1000          0                           [vdso]
      0x7fffff400000<-栈开始的位置    0x7ffffffff000   0xbff000          0                           [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000          0                   [vsyscall]
stack表示栈空间。栈空间的顶端是0x7fffff400000,然后栈指针sp的值是0x7fffff3fefe0。
因为0x7fffff3fefe0比0x7fffff400000小,说明在栈空间之外呀!!
说明栈溢出

5.栈大小的限制
$ ulimit -s
10240

将栈的大小扩大到10倍,就不会发生segmentation fault错误了
$ ulimmit -Ss 81920
$ ./sum
sum(0..1048576)=549756338176
ESP与EBP
(1)编译器在编译代码时,一边都是以ebp作为基准来定位各个参数以及局部变量的,
在函数开头执行完push ebp; mov ebp, esp;sub esp, xx;这三条典型代码后,ebp作为栈底指针是不会变的

(2)ESP是当前堆栈指针,EBP是当前堆栈帧的基指针。
调用函数时,通常会在堆栈上为局部变量保留空间。此空间通常通过EBP引用(在函数调用期间,所有局部变量和函数参数都是此寄存器的已知常量偏移量)。

另一方面,ESP在函数调用期间会随着其他函数的调用而改变,或者作为临时堆栈空间用于部分操作结果。
  • 通过栈上保存的返回地址信息,可以获得与bt结果相同的调用跟踪信息

参考:《GDB/Debug.Hacks中文版:深入调试的技术和工具》