为什么要使用 "ret" 而不是 "call" 来调用方法?
Why would one use "ret" instead of "call" to call a method?
在阅读和学习开源操作系统时,我偶然发现了一种在汇编中调用“方法”的极其复杂的方法。它使用 'ret' 指令调用库方法来执行此操作:
push rbp ; rsp[1] = rbp
mov rbp, .continue ; save return label to rbp
xchg rbp, QWORD [rsp] ; restore rbp and set rsp[1] to return label
push rbp ; rsp[0] = rbp
mov rbp, 0x0000700000000000 + LIB_PTR_TABLE.funcOffset ; rbp = pointer to func pointer
mov rbp, QWORD [rbp] ; rbp = func pointer
xchg rbp, QWORD [rsp] ; restore rbp and set rsp[0] to func pointer
; "call" library by "returning" to the address we just planted
ret
.continue:
我添加评论是为了自己理解它,看来我是对的或足够接近,因为我所做的所有实验都成功了。但后来我尝试这样做,也很完美:
mov rax, 0x0000700000000000 + LIB_PTR_TABLE.funcOffset ; rax = ptr to func ptr
mov rax, QWORD [rax] ; rax = func ptr
call rax ; actually call the library function in a normal fashion
查看指令的数量以及 CPU 在这两种情况下实际必须做的事情,人们会假设,如果速度更快,那将是“调用”变体。但是由于使用了“ret”变体并且提出这个首先需要一堆知识,那么第一个变体有什么优势呢? (或者是吗?)
随着 CPUs 变得更快,CPU 由于缓存未命中和分支预测错误等原因而停止(并且无法做任何事情)的可能性增加。为了帮助避免这些停顿,大多数现代 80x86 CPUs 有一堆逻辑来帮助预测控制流变化的目标地址;包括分支方向预测器、分支目标预测器、return 堆栈缓冲区等
问题是恶意攻击者(使用推测执行和测量时间)可以从 CPU 收集的所有信息中提取机密信息以提高性能;包括从分支方向预测器、分支目标预测器、return 堆栈缓冲区等中提取机密信息
当发现这一点时,人们(主要是内核开发人员)争先恐后地想出各种方法来缓解安全问题。具体来说,寻找避免、破坏或污染 CPU 收集的数据的方法。
更具体(针对您显示的代码);如果代码使用 call rax
,那么它会将数据添加到 CPU 的 return 堆栈缓冲区,恶意攻击者可以探测该缓冲区以确定 rax
中的原始值](如果 rax
应该是机密的,那么这就构成机密泄露)。
另一种方法是压入 return 地址,然后使用间接跳转。在这种情况下,它只会将(机密)数据留在 CPU 的分支目标缓冲区中,攻击者可以探测到这些数据,这并没有多大帮助。
使用 ret
不在 return 堆栈缓冲区(或分支目标缓冲区)中存储任何内容,从而防止出现安全问题。作为副作用,它还会“取消同步”CPU 的 return 堆栈缓冲区;混淆之前的 calls/future return 一点。
悲哀;所有这些都会导致性能问题——它让我们回到“随着 CPU 变得更快,CPU 停滞的机会增加”,并增加了从错误地址获取代码的成本摊位的费用。
在阅读和学习开源操作系统时,我偶然发现了一种在汇编中调用“方法”的极其复杂的方法。它使用 'ret' 指令调用库方法来执行此操作:
push rbp ; rsp[1] = rbp
mov rbp, .continue ; save return label to rbp
xchg rbp, QWORD [rsp] ; restore rbp and set rsp[1] to return label
push rbp ; rsp[0] = rbp
mov rbp, 0x0000700000000000 + LIB_PTR_TABLE.funcOffset ; rbp = pointer to func pointer
mov rbp, QWORD [rbp] ; rbp = func pointer
xchg rbp, QWORD [rsp] ; restore rbp and set rsp[0] to func pointer
; "call" library by "returning" to the address we just planted
ret
.continue:
我添加评论是为了自己理解它,看来我是对的或足够接近,因为我所做的所有实验都成功了。但后来我尝试这样做,也很完美:
mov rax, 0x0000700000000000 + LIB_PTR_TABLE.funcOffset ; rax = ptr to func ptr
mov rax, QWORD [rax] ; rax = func ptr
call rax ; actually call the library function in a normal fashion
查看指令的数量以及 CPU 在这两种情况下实际必须做的事情,人们会假设,如果速度更快,那将是“调用”变体。但是由于使用了“ret”变体并且提出这个首先需要一堆知识,那么第一个变体有什么优势呢? (或者是吗?)
随着 CPUs 变得更快,CPU 由于缓存未命中和分支预测错误等原因而停止(并且无法做任何事情)的可能性增加。为了帮助避免这些停顿,大多数现代 80x86 CPUs 有一堆逻辑来帮助预测控制流变化的目标地址;包括分支方向预测器、分支目标预测器、return 堆栈缓冲区等
问题是恶意攻击者(使用推测执行和测量时间)可以从 CPU 收集的所有信息中提取机密信息以提高性能;包括从分支方向预测器、分支目标预测器、return 堆栈缓冲区等中提取机密信息
当发现这一点时,人们(主要是内核开发人员)争先恐后地想出各种方法来缓解安全问题。具体来说,寻找避免、破坏或污染 CPU 收集的数据的方法。
更具体(针对您显示的代码);如果代码使用 call rax
,那么它会将数据添加到 CPU 的 return 堆栈缓冲区,恶意攻击者可以探测该缓冲区以确定 rax
中的原始值](如果 rax
应该是机密的,那么这就构成机密泄露)。
另一种方法是压入 return 地址,然后使用间接跳转。在这种情况下,它只会将(机密)数据留在 CPU 的分支目标缓冲区中,攻击者可以探测到这些数据,这并没有多大帮助。
使用 ret
不在 return 堆栈缓冲区(或分支目标缓冲区)中存储任何内容,从而防止出现安全问题。作为副作用,它还会“取消同步”CPU 的 return 堆栈缓冲区;混淆之前的 calls/future return 一点。
悲哀;所有这些都会导致性能问题——它让我们回到“随着 CPU 变得更快,CPU 停滞的机会增加”,并增加了从错误地址获取代码的成本摊位的费用。