首页 > 其他分享 >x86-64 C Calling Convention

x86-64 C Calling Convention

时间:2023-04-27 18:04:19浏览次数:35  
标签:subroutine x86 例程 Convention 指令 call 寄存器 push 64

ASM层面的例程调用

在x86-64中,指令集本身提供了用于实现子例程调用(函数调用)的一些指令。其它指令集架构,如risc-v、arm,也都提供了这些指令。

x86-64以4条核心指令提供了一个调用栈的模型,以实现子例程调用。

push指令

语法

  • push
  • push
  • push

语义

push指令将它的操作数放在内存中硬件支持栈的顶端。具体地说,它首先将RSP减少8,然后将它的操作数放到在地址[RSP]处的64位内容区。RSP(栈指针)被push递减,因为x86中栈向下增长。

Examples:

push rax    将rax中的内容压到栈上
push [var]  将在地址`var`上的8个字节放到栈上

pop指令

语法

  • pop
  • pop

语义

pop指令从硬件支持栈上移除8字节数据并放到指定的操作数上(比如寄存器或内存位置)。具体地说,pop首先移动在内存位置[RSP]上的8字节到特定的寄存器或内存位置上,然后将SP增加8。

Examples:

push rdi    将栈顶元素弹出到RDI
push [rbx]  将栈顶元素弹出到    

call指令

语法

  • call

语义

这个指令实现了一个子例程调用,它与子例程返回指令ret协作。这个指令首先将当前代码位置push到硬件支持栈上,然后执行一个非条件的跳转到label操作数指定的代码位置。该指令添加的值用于保存当子例程完成时返回的位置。

Examples:

call my_subroutine    跳转到'my_subroutine'标签,将当前的返回地址push到栈上

ret指令

语法

  • ret

语义

在于call指令的协作中,ret指令实现了一个子例程返回机制。这个指令首先从硬件支持栈中弹出一个代码位置,然后执行一个非条件跳转到获取到的代码位置。

Examples:

ret      返回到栈顶的代码位置

最小call-ret示例

section .text
    global _start

_start:
    mov rbx, 4       ; 将4存到rbx
    call subroutine  ; 调用子例程

    mov rax, 60      ; exit系统调用号
    mov rdi, rbx     ; 系统调用参数为rbx中的值,我们可以推断该进程的退出状态码肯定是14
    syscall          ; 执行系统调用

global subroutine
subroutine:
    add rbx, 10      ; 将rbx中的值加10,这次就是14了
    ret              ; 返回到_start
➜ ✗ yasm -f elf64 subroutine.asm    
# 链接可执行文件,-export-dynamic是保存符号信息,方便我们后面gdb
➜ ✗ ld -export-dynamic -s -o subroutine subroutine.o

# 执行程序并输出该程序的返回状态
➜ ✗ ./subroutine; echo $?
14

gdb调试,目前我们已经进入到_start的第一条指令。

img

目前,我们的栈顶内容还是1

img

直到call发生,栈顶指针发生了变化,向下减少了8,并且栈顶指针上的值变成了0x0040100c,就是_startcall的下一条指令:

img

至此可以说明,call按照上面的语义执行了。

如果按照ret的语义,那么,执行完ret,rsp的值应该恢复成0x7fffffffda60,并且指令流会跳回0x40100c的位置。

img

如果我在ret前往栈顶放点东西?

嘶,从callret的语义来看,如果我想它们正常协作,那么在一个例程里,我push了多少次,就得有多少次对应的pop,否则ret弹出栈顶作为例程的返回地址时会拿到错误的返回地址,也就回不到调用者原来的位置!

假如,我把subroutine修改成这样......

subroutine:
    add rbx, 10
    push subroutine  ; 把subroutine的地址放到栈顶
    ret

那不是死循环了吗?

果然,它不动了......

img

如果你用gdb调试它,会发现我们一直在subroutine的三条指令中来回跳转,很有趣。

什么是Calling Convention

上面我们大概已经知道了处理器指令集架构提供的基于硬件支持栈的例程调用模型,但是,它和高级语言中的函数、方法等概念还有一些区别。如果想用这个实现函数调用,至少还要解决一些问题:

  1. 参数如何传递给一个子例程?
  2. 子例程可以覆盖寄存器中的值吗?
  3. 调用者希望寄存器中的内容得到保持吗?
  4. 子例程中的本地变量在哪里保存?
  5. 返回值如何保存?

对于参数和返回值的保存,基于上面的栈模型,我们大概可以想到在例程相互调用的场景下,怎样进行传值。我们可以使用有限的寄存器和稍大一些的栈空间来保存。

img

我们仍有很多问题,一个例程可以清楚的知道自己使用了哪些寄存器,但是它不知道在调用链中处于上游的其它人使用了哪些寄存器,所以它能否轻易的决定覆盖一个寄存器呢?

对于几个例程之间的调用,我们当然可以约定出一套规范,编写它们时都遵循这一套规范来回答上面的问题,但若想所有库之间都能共同工作,一套更大的规范是很有必要的。C编译器在编译C代码时会按照一种规范来回答上面的问题,而这个规范就是Calling Convention,这样,所有用这套规范编译出来的代码就能协同工作了。

C在x86上的Calling Convention将一次调用的发起者(Caller)和被调用的目标例程(Callee)分开看待,利用寄存器和栈协作实现规范。对于任何语言和任何架构几乎都是一样的。

Caller

  1. caller需要向被callee传参,传参会利用到6个寄存器(rdi,rsi,rdx,rcx,r8,r9),就像开始的代码演示里的那样,caller将参数写入到一个寄存器,callee读取这个寄存器。子例程有可能覆盖这些寄存器,因为它也可能作为caller调用其它例程,所以,caller若希望后续还要用到这些寄存器,它需要自己在栈上保存这6个寄存器的值,外加r10和r11。rdi, rsi, rdx, rcx, r8, r9, r10, r11共同被称作caller-saved寄存器
  2. 如果一次函数调用的参数多于6个,将多出的反向保存在栈上,由于栈反向增长,所以第一个这样的额外参数被存在最低位置
  3. call函数会将返回地址(也就是call的下一条指令的地址)压入栈顶
  4. 当方法调用返回,caller需要将栈上保存的额外参数弹出
  5. caller可以从rax寄存器中找到子例程的返回结果
  6. caller从栈上弹出并恢复所有caller-saved寄存器的值,并且可以假设其它寄存器都没被修改,加上刚刚恢复的caller-saved,就可以当作全部寄存器都没被修改

总结一下

  1. 保存所有caller-saved寄存器
  2. 向6个参数寄存器中写入参数
  3. 倒序push额外参数
  4. call(隐式push返回地址)
  5. pop所有额外参数
  6. 通过rax读取返回值
  7. pop&restore caller-saved寄存器

Caller实践

有下面这样一段代码:

void sum(int arg1, int arg2,
         int arg3, int arg4, 
         int arg5, int arg6,
         int arg7) {

    printf("sum is => %d\n", arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7);
}

它接收七个参数,将它们相加并将结果打印。

有这样一个汇编代码框架:

section .text
    global _start
    extern sum

_start:
    call sum    ; call sum

    ; 下面的代码是为了让程序正常退出,不用管下面的代码
    mov rax, 60 ; system call for exit
    mov rdi, 0  ; exit code 0
    syscall     ; invoke operating system to exit

现在我们用gcc编译出sum的二进制文件。

gcc -g -c sum.c -o sum.o

我们的目标是按照C Calling Convention来改写汇编代码以实现sum方法的调用,稍后我们会汇编代码,并使用如下命令将它和sum.o链接。

yasm -f elf64 func_call1.asm -o func_call1.o
ld -o func_call1 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc func_call1.o sum.o /lib/x86_64-linux-gnu/libc.so.6

直接执行肯定是不行的,每次显示的数都是随机的,这取决于那些寄存器里当时的内容:

img

修改_start

_start:
	sub rsp, 8     ; 栈帧往下一格 我也不知道为啥,但是我照抄的
    mov rdi, 1     ; 将前六个参数存到指定寄存器中
    mov rsi, 2
    mov rdx, 3
    mov rcx, 4
    mov r8,  5
    mov r9,  6
    push     7     ; 第七个额外参数 入栈
    call sum       ; call sum
    pop rax        ; 弹出栈额外参数到rax,反正我们也不用

    mov rax, 60    ; system call for exit
    mov rdi, 0     ; exit code 0
    syscall        ; invoke operating system to exit

这里我们还是省略了相当多的步骤,比如我们都没保存caller-saved寄存器,也没恢复,因为我们知道_start就是系统启动的第一个函数,并且执行完立即通过exit系统调用退出。

我不知道我这里说的有没有问题,关于这些内容,我还是个小垃圾。有说错的欢迎指正。

Callee

未完...

问题解决

链接两个文件后执行,显示No such file or directory

通过readelf查看正常运行的二进制文件的解释器:

➜  p4 readelf -l peterson | grep interpreter                                                                                  [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

查看不能正常运行的二进制文件的解释器:

➜  examples git:(main) ✗ readelf -l a.out |grep interpreter                                                                   [Requesting program interpreter: /lib/ld64.so.1]

/lib/ld64.so.1好像是ld默认的行为,但是我们的系统中没有这个解释器。需要在链接时通过-dynamic-linker指定解释器。

详情查看:Can't run executable linked with libc - Stack Overflow

标签:subroutine,x86,例程,Convention,指令,call,寄存器,push,64
From: https://www.cnblogs.com/lilpig/p/17359803.html

相关文章

  • base64加密解密
    //base64加密解密不支持中文哦,会有问题varBase64={//加密encode:function(str){returnbtoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,functiontoSolidBytes(match,p1){returnString.fromCharCode('0x'......
  • 上传文件转base64
    functiongetBase64(file){ returnnewPromise((resolve,reject)=>{ constreader=newFileReader(); letfileResult=""; reader.readAsDataURL(file); //开始转 reader.onload=()=>{ fileResult=reader.result; }; //转失败......
  • 发现ROS7的CHR或者X86版本的一个BUG,测试环境如下。但是ROS6没这个问题
    测试环境esxi5.5版本在DELLR640的服务器,CPU为志强金牌6226R(估计金牌银牌的CPU都会有这个情况)的情况下,无法启动,反复重启。如下图。暂时不确定,如果升级esxi为6.0以上版本是否有这个问题! ......
  • Using base64 encoding and decoding for file transfer in AX 2012
    Base64BinDataIfyouwanttotransfersmallfiledatausingAXanddonotwanttomakeuseofsharedfoldersorfileuploading,sendingyourfiledirectlyinsideyourXMLmessageasbase64encodedstringisagoodoption.Base64givesyouthepossibil......
  • day 57 代码思想录 647. 回文子串 |
    给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。示例1:输入:"abc"输出:3解释:三个回文子串:"a","b","c"示例2:输入:"aaa"输出:6解释:6个回文子串:"a","a","a","aa&......
  • Qt+MySql开发笔记:Qt5.9.3的msvc2017x64版本编译MySql8.0.16版本驱动并Demo连接数据库
    前言  mysql驱动版本msvc2015x32版本调好,mysql的mingw32版本的驱动上一个版本编译并测试好,有些三方库最低支持vs2017,所以只能使用msvc2017x64,基于Qt5.9.3,于是本篇编译mysql驱动的msvc2017x64版本,满足当前的特定需求,这次过程有点费劲,可能是Qt的版本低于Qt5.12,继续无保留分享......
  • LeetCode 1643. 第 K 条最小指令
    康托展开一开始无脑枚举全排列,果断超时,还是得看看如果降低计算量。题目destination=[2,3],相当于2个V,3个H,输出全排列去重后的对应位置字典序列内容。忽略去重则问题为全排列,所有可能为:\[(\sumdestination)!=(2+3)!=5!\]k恰好为康托展开结果+1,直接逆向......
  • Base64 编码的字符串转换为 Blob 对象方法
    constblob=function(data:string,mime:string){data=data.split(',')[1];data=window.atob(data);letia=newUint8Array(data.length);for(vari=0;i<data.length;i++){ia[i]=data.charCodeAt(i);};returnnew......
  • 全球首发:Tiny10 2023 x86最终版及类似win10精简版/Win K/N版 单独添加Windows Media P
    情况:1.Windows功能列表中没有媒体功能(MediaFeatures),或该项下没有Windowsmediaplayer选项2.普通在Windows-设置-功能-可选功能中单独可以添加WMP,但实际并无效果,该组件需要相关功能包打开情况下才可以安装成功,否则尽管显示已安装,但实际Program目录下并无WindowsMediaPlayer出......
  • POJ - 3764 XOR&&dfs 01字典树
    Inanedge-weightedtree,thexor-lengthofapathpisdefinedasthexorsumoftheweightsofedgesonp:{xor}length§=\oplus{e\inp}w(e)⊕isthexoroperator.Wesayapaththexor-longestpathifithasthelargestxor-length.Givenanedge-weigh......