试图对一个函数进行逆向工程
Trying to reverse engineer a function
我想更多地了解 x86 中的汇编。我在这里有一个神秘的函数,我知道 returns 一个 int
并接受一个 int
参数。
所以它看起来像 int mystery(int n){}
。但是我无法弄清楚C中的功能。程序集是:
mov %edi, %eax
lea 0x0(,%rdi, 8), %edi
sub %eax, %edi
add [=10=]x4, %edi
callq < mystery _util >
repz retq
< mystery _util >
mov %edi, %eax
shr %eax
and [=10=]x1, %edi
and %edi, %eax
retq
我不明白 lea 在这里做什么,它可能是什么功能。
LEA 只是一个 left-shift 乘以 3,并将结果截断为 32 位(即 zero-extending EDI 隐含为 RDI)。 x86-64 System V 在 RDI 中传递了第一个整数 arg,因此所有这些都与一个 int
arg 一致。 LEA 使用 memory-operand 语法和机器编码,. Using it as part of a multiply by a constant .
生成此函数的编译器在这里错过了优化;第一个 mov
可以用
避免
lea 0x0(,%rdi, 8), %eax # n << 3 = n*8
sub %edi, %eax # eax = n*7
lea 4(%rax), %edi # rdi = 4 + n*7
但是,编译器却卡在了在 %edi
中生成 n*7
,可能是因为它对常量乘法应用了窥孔优化,来不及重做寄存器分配。
mystery_util
returns 其 arg 的低 2 位的按位与,在低位,因此是 0 或 1 整数值,也可以是 bool
.
(shr
没有计数意味着计数为 1;请记住 x86 有一个特殊的移位操作码,隐式计数为 1。8086 只有计数 1 或 cl
;立即数后来添加了计数作为扩展,implicit-form 操作码仍然更短。)
LEA
执行地址计算,但不是取消引用地址,而是将计算出的地址存储到目标寄存器中。
在 AT&T 语法中,lea C(b,c,d), reg
表示 reg = C + b + c*d
,其中 C
是常数,b
、c
是寄存器,d
是来自 { 1,2,4,8}。因此,您可以了解为什么 LEA 在简单的数学运算中很受欢迎:它在一条指令中完成了相当多的工作。 (*包括以下 prl 评论的更正)
此汇编代码有一些奇怪的特征:repz
前缀仅在应用于某些指令时才严格定义,而 retq
不是其中之一(尽管一般行为处理器将忽略它)。有关详细信息,请参阅下面带有 link 的 Michael Petch 评论。使用 lea (,rdi,8), edi
后跟 sub eax, edi
来计算 arg1 * 7
似乎也很奇怪,但是一旦 prl 注意到标量 d
必须是 2 的常数次幂就有意义了。在任何情况下案例,这是我阅读片段的方式:
mov %edi, %eax ; eax = arg1
lea 0x0(,%rdi, 8), %edi ; edi = arg1 * 8
sub %eax, %edi ; edi = (arg1 * 8) - arg1 = arg1 * 7
add [=10=]x4, %edi ; edi = (arg1 * 7) + 4
callq < mystery _util > ; call mystery_util(arg1 * 7 + 4)
repz retq ; repz prefix on return is de facto nop.
< mystery _util >
mov %edi, %eax ; eax = arg1
shr %eax ; eax = arg1 >> 1
and [=10=]x1, %edi ; edi = 1 iff arg1 was odd, else 0
and %edi, %eax ; eax = 1 iff smallest 2 bits of arg1 were both 1.
retq
注意第 4 行的 +4
完全是虚假的。它不能影响 mystery_util 的结果。
所以,总的来说,这个 ASM 片段计算了布尔值 (arg1 * 7) % 4 == 3。
汇编代码似乎是计算机生成的,并且可能是由 GCC 编译的,因为在无条件分支 (call
) 之后有一个 repz retq
。还有一个迹象表明,因为在转到 mystery_util
时没有尾调用 (jmp
) 而不是 call
,所以代码是用 -O1
编译的(更高优化级别可能会内联这里没有发生的函数)。缺少帧指针和额外的 load/stores 表明它不是用 -O0
编译的
x
乘以 7 等同于 x
乘以 8 再减去 x
。这就是以下代码所做的:
lea 0x0(,%rdi, 8), %edi
sub %eax, %edi
LEA 可以计算地址,但它也可以用于简单的算术运算。内存操作数的语法是位移(基数、索引、比例)。 scale 可以是 1, 2, 4, 8。计算是 displacement + base + index * scale。在你的情况下 lea 0x0(,%rdi, 8), %edi
实际上是 EDI = 0x0 + RDI * 8 或 EDI = RDI * 8。完整的计算是 n * 7 - 4;
mystery_util
的计算似乎只是
n &= (n>>1) & 1;
如果我将所有这些因素综合考虑,我们得到一个函数 mystery
,它将 n * 7 - 4 传递给一个名为 mystery_util
的函数,该函数 return 为 n &= (n>>1) & 1
。
由于 mystery_util
return 是单个位值(0 或 1),因此 bool
是 return 类型是合理的。
我很好奇是否可以获得具有优化级别 1 (-O1
) 的特定版本的 GCC 来重现此汇编代码。我发现 GCC 4.9.x 将为给定的 C 程序生成此 精确的汇编代码 :
#include<stdbool.h>
bool mystery_util(unsigned int n)
{
n &= (n>>1) & 1;
return n;
}
bool mystery(unsigned int n)
{
return mystery_util (7*n+4);
}
汇编输出为:
mystery_util:
movl %edi, %eax
shrl %eax
andl , %edi
andl %edi, %eax
ret
mystery:
movl %edi, %eax
leal 0(,%rdi,8), %edi
subl %eax, %edi
addl , %edi
call mystery_util
rep ret
您可以在 godbolt 上玩这个代码。
重要更新 - 没有布尔值的版本
我显然在解释这个问题时犯了错误。我假设问这个问题的人自己确定 mystery
的原型是 int mystery(int n)
。我以为我可以改变它。根据一天后在 Stackoverflow 上询问的 related question,似乎 int mystery(int n)
作为原型作为作业的一部分提供给您。这很重要,因为这意味着必须进行修改。
需要进行的更改与mystery_util
有关。在要进行逆向工程的代码中有以下几行:
mov %edi, %eax
shr %eax
EDI是第一个参数。 SHR是逻辑右移。如果 EDI 是 unsigned int
(或等价物),编译器只会生成它。 int
是有符号类型,会生成 SAR(算术右移)。这意味着 mystery_util
的参数必须是 unsigned int
(因此 return 值可能是 unsigned int
。这意味着代码将如下所示:
unsigned int mystery_util(unsigned int n)
{
n &= (n>>1) & 1;
return n;
}
int mystery(int n)
{
return mystery_util (7*n+4);
}
mystery
现在有你的教授给出的原型(bool
被删除)我们使用 unsigned int
作为参数和 return 类型的 mystery_util
.为了使用 GCC 4.9.x 生成此代码,我发现您需要使用 -O1 -fno-inline
。可以在 godbolt 上找到此代码。程序集输出与使用 bool
的版本相同。
如果你使用unsigned int mystery_util(int n)
你会发现它并没有完全输出我们想要的:
mystery_util:
movl %edi, %eax
sarl %eax ; <------- SAR (arithmetic shift right) is not SHR
andl , %edi
andl %edi, %eax
ret
我想更多地了解 x86 中的汇编。我在这里有一个神秘的函数,我知道 returns 一个 int
并接受一个 int
参数。
所以它看起来像 int mystery(int n){}
。但是我无法弄清楚C中的功能。程序集是:
mov %edi, %eax
lea 0x0(,%rdi, 8), %edi
sub %eax, %edi
add [=10=]x4, %edi
callq < mystery _util >
repz retq
< mystery _util >
mov %edi, %eax
shr %eax
and [=10=]x1, %edi
and %edi, %eax
retq
我不明白 lea 在这里做什么,它可能是什么功能。
LEA 只是一个 left-shift 乘以 3,并将结果截断为 32 位(即 zero-extending EDI 隐含为 RDI)。 x86-64 System V 在 RDI 中传递了第一个整数 arg,因此所有这些都与一个 int
arg 一致。 LEA 使用 memory-operand 语法和机器编码,
生成此函数的编译器在这里错过了优化;第一个 mov
可以用
lea 0x0(,%rdi, 8), %eax # n << 3 = n*8
sub %edi, %eax # eax = n*7
lea 4(%rax), %edi # rdi = 4 + n*7
但是,编译器却卡在了在 %edi
中生成 n*7
,可能是因为它对常量乘法应用了窥孔优化,来不及重做寄存器分配。
mystery_util
returns 其 arg 的低 2 位的按位与,在低位,因此是 0 或 1 整数值,也可以是 bool
.
(shr
没有计数意味着计数为 1;请记住 x86 有一个特殊的移位操作码,隐式计数为 1。8086 只有计数 1 或 cl
;立即数后来添加了计数作为扩展,implicit-form 操作码仍然更短。)
LEA
执行地址计算,但不是取消引用地址,而是将计算出的地址存储到目标寄存器中。
在 AT&T 语法中,lea C(b,c,d), reg
表示 reg = C + b + c*d
,其中 C
是常数,b
、c
是寄存器,d
是来自 { 1,2,4,8}。因此,您可以了解为什么 LEA 在简单的数学运算中很受欢迎:它在一条指令中完成了相当多的工作。 (*包括以下 prl 评论的更正)
此汇编代码有一些奇怪的特征:repz
前缀仅在应用于某些指令时才严格定义,而 retq
不是其中之一(尽管一般行为处理器将忽略它)。有关详细信息,请参阅下面带有 link 的 Michael Petch 评论。使用 lea (,rdi,8), edi
后跟 sub eax, edi
来计算 arg1 * 7
似乎也很奇怪,但是一旦 prl 注意到标量 d
必须是 2 的常数次幂就有意义了。在任何情况下案例,这是我阅读片段的方式:
mov %edi, %eax ; eax = arg1
lea 0x0(,%rdi, 8), %edi ; edi = arg1 * 8
sub %eax, %edi ; edi = (arg1 * 8) - arg1 = arg1 * 7
add [=10=]x4, %edi ; edi = (arg1 * 7) + 4
callq < mystery _util > ; call mystery_util(arg1 * 7 + 4)
repz retq ; repz prefix on return is de facto nop.
< mystery _util >
mov %edi, %eax ; eax = arg1
shr %eax ; eax = arg1 >> 1
and [=10=]x1, %edi ; edi = 1 iff arg1 was odd, else 0
and %edi, %eax ; eax = 1 iff smallest 2 bits of arg1 were both 1.
retq
注意第 4 行的 +4
完全是虚假的。它不能影响 mystery_util 的结果。
所以,总的来说,这个 ASM 片段计算了布尔值 (arg1 * 7) % 4 == 3。
汇编代码似乎是计算机生成的,并且可能是由 GCC 编译的,因为在无条件分支 (call
) 之后有一个 repz retq
。还有一个迹象表明,因为在转到 mystery_util
时没有尾调用 (jmp
) 而不是 call
,所以代码是用 -O1
编译的(更高优化级别可能会内联这里没有发生的函数)。缺少帧指针和额外的 load/stores 表明它不是用 -O0
x
乘以 7 等同于 x
乘以 8 再减去 x
。这就是以下代码所做的:
lea 0x0(,%rdi, 8), %edi
sub %eax, %edi
LEA 可以计算地址,但它也可以用于简单的算术运算。内存操作数的语法是位移(基数、索引、比例)。 scale 可以是 1, 2, 4, 8。计算是 displacement + base + index * scale。在你的情况下 lea 0x0(,%rdi, 8), %edi
实际上是 EDI = 0x0 + RDI * 8 或 EDI = RDI * 8。完整的计算是 n * 7 - 4;
mystery_util
的计算似乎只是
n &= (n>>1) & 1;
如果我将所有这些因素综合考虑,我们得到一个函数 mystery
,它将 n * 7 - 4 传递给一个名为 mystery_util
的函数,该函数 return 为 n &= (n>>1) & 1
。
由于 mystery_util
return 是单个位值(0 或 1),因此 bool
是 return 类型是合理的。
我很好奇是否可以获得具有优化级别 1 (-O1
) 的特定版本的 GCC 来重现此汇编代码。我发现 GCC 4.9.x 将为给定的 C 程序生成此 精确的汇编代码 :
#include<stdbool.h>
bool mystery_util(unsigned int n)
{
n &= (n>>1) & 1;
return n;
}
bool mystery(unsigned int n)
{
return mystery_util (7*n+4);
}
汇编输出为:
mystery_util:
movl %edi, %eax
shrl %eax
andl , %edi
andl %edi, %eax
ret
mystery:
movl %edi, %eax
leal 0(,%rdi,8), %edi
subl %eax, %edi
addl , %edi
call mystery_util
rep ret
您可以在 godbolt 上玩这个代码。
重要更新 - 没有布尔值的版本
我显然在解释这个问题时犯了错误。我假设问这个问题的人自己确定 mystery
的原型是 int mystery(int n)
。我以为我可以改变它。根据一天后在 Stackoverflow 上询问的 related question,似乎 int mystery(int n)
作为原型作为作业的一部分提供给您。这很重要,因为这意味着必须进行修改。
需要进行的更改与mystery_util
有关。在要进行逆向工程的代码中有以下几行:
mov %edi, %eax
shr %eax
EDI是第一个参数。 SHR是逻辑右移。如果 EDI 是 unsigned int
(或等价物),编译器只会生成它。 int
是有符号类型,会生成 SAR(算术右移)。这意味着 mystery_util
的参数必须是 unsigned int
(因此 return 值可能是 unsigned int
。这意味着代码将如下所示:
unsigned int mystery_util(unsigned int n)
{
n &= (n>>1) & 1;
return n;
}
int mystery(int n)
{
return mystery_util (7*n+4);
}
mystery
现在有你的教授给出的原型(bool
被删除)我们使用 unsigned int
作为参数和 return 类型的 mystery_util
.为了使用 GCC 4.9.x 生成此代码,我发现您需要使用 -O1 -fno-inline
。可以在 godbolt 上找到此代码。程序集输出与使用 bool
的版本相同。
如果你使用unsigned int mystery_util(int n)
你会发现它并没有完全输出我们想要的:
mystery_util:
movl %edi, %eax
sarl %eax ; <------- SAR (arithmetic shift right) is not SHR
andl , %edi
andl %edi, %eax
ret