简单理解函数的调用

函数的调用也是有约定的,比如传递参数的方式,返回值返回的方式等。

一些前提知识

不同的平台编译器用的约定也有可能有不同,详细请参照wiki,x86 Calling Convention

不同的平台编译器可能对Caller和Callee的职责有区别。详细请参照上述链接,下面主要讨论x86-64(AMD64) 在Linux 下的calling convention。其他平台暂不涉及。

calling_convention.png

System V AMD64 ABI

系统V AMD64的调用约定跟随着Solaris, Linux, FreeBSD, maxOS, 并且实际上已经是那些Unix和类Unix操作系统的标准。

  • 函数参数的前6个整数或者指针参数会被填入RDI, RSI, RDX, RCX, R8, R9(R10被用于作为一个静态的链指针以防有嵌套的调用)
  • 像Microsoft x64的调用约定一样,额外的参数被压入到栈中。
  • 整数返回值在64bits之内都通过RAX返回。如果是128bits,通过RAX和RDX。

Caller 清理Stack 的意思是:
Caller传入栈中的参数,需要Caller自己去清理掉,维持堆栈平衡,这里有个表格:


register list

在 x86-64 Linux, %rbp, %rbx, %r12, %r13, %r14, %r15 and %rsp 是 callee-saved 的寄存器, 其它的称为caller-saved 寄存器。

(图中的caller-owned 即 callee-saved 寄存器,callee-owned 即 caller-saved寄存器)
caller-saved register: 调用者保存寄存器
即如果调用者在调用其他函数之后仍旧想使用调用之前的寄存器的值,那么调用者需要自己保存这些寄存器的值,被调用者无须关心这些,可以直接使用,因为它知道如果调用者还想用这些寄存器里面的值的话,自己已经保存过了。
这一类寄存器又称为 临时的寄存器。在过程调用中,编译器不会自动为你在函数调用之间保存/恢复这些寄存器的值。

callee-saved register: 被调用者保存寄存器
即被调用的函数中,如果想使用这些寄存器,那么必须先将寄存器中的值保存下来后才能使用,并且在返回调用者之前,将寄存器的值恢复。调用者不必担心这些寄存器的值被修改,因为它知道被调用的函数会保护这些寄存器的值。
这一类寄存器又称为 调用保护的寄存器。编译器会帮你保存这些寄存器的值。

cdecl (C declaration):
在C语言中,函数的参数是被从右至左依次按顺序push到栈中的,也就是 最后一个参数先被 push。在Linux,GCC 实际上将其设为标准的调用约定。从GCC 4.5版本起,栈必须按照16字节进行对齐(之前的版本只是需要4字节对齐) 。

RTL(C):
Right to Left 顺序参数入栈。

以上均为翻译wiki,各位看官可以直接看原文哈~

一些简单指令的含义

时刻记住栈的增长方向是从高地址向低地址

  1. push xxx
    将xxx压入到栈中,此时%rsp 的值会增长(这里的增长就是给rsp减掉xxx的大小)对应大小
  2. call xxx
    call 指令做2个事情
  • 将下一条指令的地址push到栈中(即返回地址入栈)
  • jmp到 xxx 地址 执行指令
  1. leave
  • 设置%rsp的值为%rbp,并从栈中pop出来的值赋值给rbp。相当于mov %rbp, %rsp; pop %rbp ;
  1. ret
  • 将程序控制转换到到当前栈顶的地址上。就是pop当前栈顶到%rip。相当于pop %rip;这个地址一般是call指令放置的。即回到了call指令之后的指令上。

这里有个详细的Guide : http://web.stanford.edu/class/cs107/guide/x86-64.html

简单的实践-手动读取函数的参数

有了以上的基础,看一个简单的函数:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void foo3(long a, long b, long c, long d, long e, long f, long g, long h , long i) {
    long j,k,l,m,n,o,p,q,r ;
    // 通过寄存器和栈读取传入参数
    __asm__ ("movq %%rdi, %0\n\t"
                          "movq %%rsi, %1\n\t"
                          "movq %%rdx, %2\n\t"
                          "movq %%rcx, %3\n\t"
                          "movq %%r8, %4\n\t"
                          "movq %%r9, %5\n\t"
                          "movq 0x10(%%rbp), %%rax\n\t"
                          "movq %%rax, %6\n\t"
                          "movq 0x18(%%rbp), %%rax\n\t"
                          "movq %%rax, %7\n\t"
                          "movq 0x20(%%rbp), %%rax\n\t"
                          "movq %%rax, %8\n\t"
                          : "=m"(j), "=m"(k), "=m"(l), "=m"(m),"=m"(n), "=m"(o), "=m"(p), "=m"(q), "=m"(r)
                          ::"%rax") ;
    printf("%ld, %ld, %ld, %ld, %ld, %ld, %ld, %ld, %ld\n", j, k,l,m,n,o,p,q,r) ;
}


int main() {
    // 这里的call指令会将下一条指令的地址入栈
    // jump 到foo3
    foo3(0,1,2,3,4,5,6,7,8) ;

    return 0 ;
}

按照上述的x86_64(AMD64) 在Linux 上的call convention,我们的foo3函数有9个参数,其中6个参数将放置到寄存器中,剩余的3个参数将放到栈中。

注:栈的增长是从高地址向低地址
main中调用foo3的参数传递,依据顺序 RTL,因此依次如下:

pushq 0x8
pushq 0x7
pushq 0x6

三个参数push到栈完成。
剩下的6个,赋值给寄存器:

movq $0x5, %r9
movq $0x4, %r8
movq $0x3, %rcx
movq $0x2, %rdx
movq $0x1, %rsi
movq $0x0, %rdi

之后的操作就是:
call foo3

call foo3 做两件事:

  1. push 下一条指令的地址到栈中
  2. jmp 到foo3的地址开始执行(等同于将%rip的值设置成foo3的地址)

objdump -D a.out > a.s

不习惯看AT&T汇编的,可以
objdump -D a.out -M intel > a.s
用intel语法看

我们可以看看反汇编之后的main函数是什么样:

000000000040121e <main>:
; 保存rbp值到栈中
  40121e:   55                      push   %rbp
; 让rbp指向rsp 
  40121f:   48 89 e5                mov    %rsp,%rbp
; 栈增长8个字节, 这个是为了和后续的3个push连起来形成16字节对齐
  401222:   48 83 ec 08             sub    $0x8,%rsp
; 压参数
  401226:   6a 08                   pushq  $0x8 
  401228:   6a 07                   pushq  $0x7 
  40122a:   6a 06                   pushq  $0x6 
; %r9d 标志 %r9 的低32位
  40122c:   41 b9 05 00 00 00       mov    $0x5,%r9d
  401232:   41 b8 04 00 00 00       mov    $0x4,%r8d
  401238:   b9 03 00 00 00          mov    $0x3,%ecx
  40123d:   ba 02 00 00 00          mov    $0x2,%edx
  401242:   be 01 00 00 00          mov    $0x1,%esi
  401247:   bf 00 00 00 00          mov    $0x0,%edi
  40124c:   e8 34 ff ff ff          callq  401185 <foo3>
; 这里就体现出了caller清理参数栈空间,总共push了3次8字节,对齐时多分了8字节,总共32字节
; 所以这里给 %rsp + 0x20 表示栈回退了32字节
  401251:   48 83 c4 20             add    $0x20,%rsp

栈的布局如下:

main 栈空间

因为是caller清理参数栈,所以图中8,7,6也属于main的栈空间。

在foo3中,foo3 首先要保存当前的栈基指针%rbp,因为此时的%rbp 的值是main函数的栈开始的位置,必须保存起来。在函数中对栈内变量的赋值是通过 %rbp +/- 偏移 来操作的。所以此时应该将 %rsp 的值赋值给 %rbp ,这样就能在该函数中通过对%rbp +/- 偏移,来操作该函数中的局部变量。

可以简单看一下foo3反汇编后的部分代码:

0000000000401185 <foo3>:
  401185:   55                      push   %rbp
  401186:   48 89 e5                mov    %rsp,%rbp
  401189:   48 83 c4 80             add    $0xffffffffffffff80,%rsp
  ; 以下可以先不看
  40118d:   48 89 7d a8             mov    %rdi,-0x58(%rbp)
  401191:   48 89 75 a0             mov    %rsi,-0x60(%rbp)
  401195:   48 89 55 98             mov    %rdx,-0x68(%rbp)
  401199:   48 89 4d 90             mov    %rcx,-0x70(%rbp)
  40119d:   4c 89 45 88             mov    %r8,-0x78(%rbp)
  4011a1:   4c 89 4d 80             mov    %r9,-0x80(%rbp)
  ... ...

push %rbp // 保存rbp的值到栈中
mov %rsp, %rbp // rbp和rsp指向同一个位置
add $0xffffffffffffff80, %rsp // 其实就是rsp-128,即栈空间扩展128字节,为栈中的变量使用
下面的指令先不看了,此时我们再看看栈空间:

栈空间

那么我们读取6,7,8的值就很直观了,
%rbp-0x10就可以读到6
%rbp-0x18就可以读到7
%rbp-0x20就可以读到8

这样,我们的代码就可以理解了。(因为movq的两个操作数不允许同时为内存,所以通过%rax 转存了一下)。

验证返回值放在哪里些寄存器就留给各位看官了。

既然我们可以通过偏移来直接读取栈中保存的参数,那么当然也可以通过偏移改动栈中的值。接下来我们就通过修改函数的返回地址,来让一个没有被调用的函数,被调用到。

简单的实践-函数返回地址修改

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void foo2() {
    printf("wow you got call me ...\n") ;
    // 这里最好直接退出,因为foo2返回时,地址不知道是什么了 ... ...
    exit(-1) ;
}

void foo() {
    long *p = NULL ;
    p = (long*)&p ;
    // 修改caller 保存的返回地址,改成foo2函数的地址
    *(p+2) = (long)foo2 ;
}

int main() {
    // 这里的call指令会将下一条指令的地址入栈
    // jump 到foo
    foo() ;

    return 0 ;
}

有了上述的例子作为基础,就不难理解foo() 中为什么能够成功了。

[tutu@localhost c_test]$ ./a.out 
wow you got call me ...
[tutu@localhost c_test]$ 

同样的,我们借助rbp的值+8也能够实现这样的赋值。

void jump_to_foo2() {
    long *ptr = (long*)&foo2 ;
    __asm__ ("movq %0, %%rax\n\t"
             "movq %%rax, 8(%%rbp)"
             :
             :"m"(ptr)
             :) ;
}

由此,各类缓冲区溢出攻击层出不穷了 ... ... 后续有时间可以写写shellcode ...

感谢各位看官 ~

The End ;

windleaves
2020-04-21 00:51:00

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容

  • 1. 汇编寄存器调用约定 1.1 汇编101 看看下面的汇编片段: 在这段汇编代码中有三个操作码pushq、sub...
    收纳箱阅读 2,615评论 3 1
  • 由于不是科班出生,又是自学开发,对很多方面的知识都是只知其然而不知其所以然。加上最近公司事情不多,刚好乘此机会把长...
    寒咯阅读 13,009评论 3 8
  • 本文首发于我的博客 Bomb Lab 实验代码见GitHub 简介 BombLab是CS:APP中对应第三章内容:...
    viseator阅读 14,268评论 0 14
  • 体系介绍 SSP帮助媒体管理广告栏位及广告展现 上传展示广告 依业务规则控制广告流量 匹配广告、访客及上下文 策略...
    vastorman阅读 207评论 0 0
  • 暑假的麦田里总是金灿灿的一片,像是舞蹈的千手观音。弟弟跟着他的屁股后面在地埂上撒了一泡尿,手抓起来和泥。他不理会弟...
    史叁阅读 303评论 0 0