CSAPP - 反汇编 strings_not_equal
CSAPP bomlab1 中涉及到的 strings_not_equal
函数, 虽然可以从函数名字猜出函数含义,但我想根据汇编代码反推出对应的C代码,而不是根据函数名字猜测。
相比于专门学习 CTF 的选手, 本篇的废话很多,是完全不熟悉汇编的视角出发。
一点经验:
- 逐句翻译汇编,写出对应的C代码
- 写出C代码的时候,增加注释,把寄存器 和 变量名字 绑定起来, 好处是下次看到 callee-saved 寄存器时,直接对应到C代码
概况 - 涉及的函数调用
int main()
{
phase_1()
- read_line()
- strings_not_equal()
- string_length()
}
完整的反汇编代码
前文已对 string_length() 做了反汇编(汇编代码到C代码的人工翻译和整理), 本篇对 strings_not_equal() 做反汇编, 它的完整汇编代码是:
(gdb) disassemble strings_not_equal
Dump of assembler code for function strings_not_equal:
0x0000000000401338 <+0>: push r12
0x000000000040133a <+2>: push rbp
0x000000000040133b <+3>: push rbx
0x000000000040133c <+4>: mov rbx,rdi
0x000000000040133f <+7>: mov rbp,rsi
0x0000000000401342 <+10>: call 0x40131b <string_length>
0x0000000000401347 <+15>: mov r12d,eax
0x000000000040134a <+18>: mov rdi,rbp
0x000000000040134d <+21>: call 0x40131b <string_length>
0x0000000000401352 <+26>: mov edx,0x1
0x0000000000401357 <+31>: cmp r12d,eax
0x000000000040135a <+34>: jne 0x40139b <strings_not_equal+99>
0x000000000040135c <+36>: movzx eax,BYTE PTR [rbx]
0x000000000040135f <+39>: test al,al
0x0000000000401361 <+41>: je 0x401388 <strings_not_equal+80>
0x0000000000401363 <+43>: cmp al,BYTE PTR [rbp+0x0]
0x0000000000401366 <+46>: je 0x401372 <strings_not_equal+58>
0x0000000000401368 <+48>: jmp 0x40138f <strings_not_equal+87>
0x000000000040136a <+50>: cmp al,BYTE PTR [rbp+0x0]
0x000000000040136d <+53>: nop DWORD PTR [rax]
0x0000000000401370 <+56>: jne 0x401396 <strings_not_equal+94>
0x0000000000401372 <+58>: add rbx,0x1
0x0000000000401376 <+62>: add rbp,0x1
0x000000000040137a <+66>: movzx eax,BYTE PTR [rbx]
0x000000000040137d <+69>: test al,al
0x000000000040137f <+71>: jne 0x40136a <strings_not_equal+50>
0x0000000000401381 <+73>: mov edx,0x0
0x0000000000401386 <+78>: jmp 0x40139b <strings_not_equal+99>
0x0000000000401388 <+80>: mov edx,0x0
0x000000000040138d <+85>: jmp 0x40139b <strings_not_equal+99>
0x000000000040138f <+87>: mov edx,0x1
0x0000000000401394 <+92>: jmp 0x40139b <strings_not_equal+99>
0x0000000000401396 <+94>: mov edx,0x1
0x000000000040139b <+99>: mov eax,edx
0x000000000040139d <+101>: pop rbx
0x000000000040139e <+102>: pop rbp
0x000000000040139f <+103>: pop r12
0x00000000004013a1 <+105>: ret
End of assembler dump.
由于汇编较长,并且在执行时跳来跳去,像是乱划线,因此按照汇编代码相对于函数起始地址的偏移量(例如 +8
) 作为序号进行解释。一共105个序号,其中很多相邻行可以合并起来看。
偏移地址 0, 2, 3 和偏移地址 101, 102, 103 的汇编解读
0x0000000000401338 <+0>: push r12
0x000000000040133a <+2>: push rbp
0x000000000040133b <+3>: push rbx
...
0x000000000040139d <+101>: pop rbx
0x000000000040139e <+102>: pop rbp
0x000000000040139f <+103>: pop r12
+0: push r12
: 把 r12 寄存器内容压栈。对应的是 +103
的 pop 103
, 是从栈上恢复 r12 寄存器。
为啥要压栈和恢复?因为 r12 寄存器要被当前函数 strings_not_equal 使用, 而调用规约规定, callee 要在函数结束时恢复 r12, 这样的话,前面一个调用 r12 的函数(frame)能继续用 r12。
+2: push rbp
: 将基指针寄存器(Base Pointer,rbp)的内容压入栈中。rbp 通常用于标记当前栈帧的开始,它在函数调用过程中用于定位局部变量和函数参数。
显然, 先压入 r12, 再压入 rbp 是比较合理的。
对应的是 +102
的 pop rbp
, 弹出 rbp。
+3 push rbx
: 将 rbx 寄存器的内容压入栈中。rbx 是另一个被调用者保存寄存器,它在函数执行过程中用于保存一个临时值,或者作为一个指向数据的指针。
对应的是 +101
的 pop rbx
, 恢复 rbx 寄存器。
callee-saved寄存器
在 x86_64 架构中,使用的是 System V AMD64 ABI 调用约定(这是大多数 Unix-like 系统,包括 Linux 和 macOS,所采用的)。根据这个调用约定,以下是被调用者保存(callee-saved)寄存器的列表:
RBX - 基址寄存器(Base register)
RBP - 基指针寄存器(Base pointer register)
R12 - 第12个通用寄存器
R13 - 第13个通用寄存器
R14 - 第14个通用寄存器
R15 - 第15个通用寄存器
RSP - 栈指针寄存器(Stack pointer register),虽然通常不直接保存和恢复,但必须在函数调用结束时保持一致。
被调用者保存的寄存器是指在函数调用过程中,如果一个函数需要修改这些寄存器,它必须在函数返回前将它们恢复到原始值。这意味着调用者可以期望这些寄存器在函数调用后保持不变。
caller-saved寄存器
除此之外,还有一些寄存器是调用者保存(caller-saved)的,也被称作易失性(volatile)寄存器,包括:
- RAX - 累加器寄存器(Accumulator):用于整数运算和返回值。
- RCX - 计数寄存器(Counter):用于字符串操作和循环计数。
- RDX - 数据寄存器(Data register):用于整数运算和输入/输出操作。
- RSI - 源索引寄存器(Source Index):在字符串和数组操作中用作源地址指针。
- RDI - 目的索引寄存器(Destination Index):在字符串和数组操作中用作目的地址指针。
- R8 - 第8个通用寄存器:用于整数运算和传递函数参数。
- R9 - 第9个通用寄存器:同样用于整数运算和传递函数参数。
- R10 - 第10个通用寄存器:通常用于整数运算。
- R11 - 第11个通用寄存器:通常用于整数运算。
- XMM0-XMM15(浮点寄存器(用于浮点数和 SIMD 运算):
- XMM0-XMM7 - 用于浮点运算和 SIMD 运算的寄存器,同时也用于传递函数参数和返回值。
- XMM8-XMM15 - 在某些系统上,这些额外的寄存器用于相同的目的,但主要是在函数调用中作为易失性寄存器使用。
这些寄存器可以在函数调用中被自由修改,不需要保存和恢复它们的原始值。如果调用者希望在函数调用后使用这些寄存器的值,它必须在调用之前自己保存它们。
偏移地址 4, 7
0x000000000040133c <+4>: mov rbx,rdi
0x000000000040133f <+7>: mov rbp,rsi
前面提到 rbx 和 rbp 都是 callee-saved 寄存器, 已经在使用前保存原有值、 在函数结束时恢复原有值。
现在开始使用 rbx 和 rbp:
mov rbx, rdi
: rdi 寄存器存放了函数 strings_not_equal() 第一个参数。这句汇编是意思是把第一个参数放到 rbx 寄存器里。
mov rbp, rsi
: 类似上面这句, 是把第二个参数放到 rbp 寄存器里。
尝试写一下对应的 C 代码:
return_type strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1;
const char* p2 = s2;
}
偏移地址 94, 99 - 函数返回值类型
0x0000000000401396 <+94>: mov edx,0x1
0x000000000040139b <+99>: mov eax,edx
在汇编代码 ret 之前紧邻的代码中, 如果是往 eax 寄存器里写东西, 那说明函数返回值类型是 int。 eax 是用于 int 类型返回值的函数的寄存器。
mov edx, 0x1
: 把数字1写入到 rdx 寄存器。前面提到过, rdx 是数据寄存器(Data register),用于整数运算和输入/输出操作。
mov eax, edx
: 把 edx 寄存器内容写入到 eax 寄存器。
通过 edx 寄存器传递感觉是多余的,但由于不知道 CSAPP bomb 可执行文件的编译参数中,是否开启过优化选项, 因此不能确定能否直接干掉。
对应的 C 代码,更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1;
const char* p2 = s2;
return 1;
}
偏移地址 10, 15, 18, 21, 26, 31, 34, 99 - 调用 string_length
0x0000000000401342 <+10>: call 0x40131b <string_length>
0x0000000000401347 <+15>: mov r12d,eax
0x000000000040134a <+18>: mov rdi,rbp
0x000000000040134d <+21>: call 0x40131b <string_length>
0x0000000000401352 <+26>: mov edx,0x1
0x0000000000401357 <+31>: cmp r12d,eax
0x000000000040135a <+34>: jne 0x40139b <strings_not_equal+99>
0x000000000040139b <+99>: mov eax,edx
call 0x40131b <string_length>
: 调用 C 语言函数 string_length. 前文分析过它, 返回值类型是 int, 也就是说返回值放在 eax 寄存器中。
mov r12d,eax
: 前面提到过, r12 是 callee-saved register, 用来存放数据。这里是说,把调用 string_length(s1) 的结果,存放到 r12d。 此时 C 代码大概这样:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
return 1;
}
mov rdi, rbp
: 把 rbp 寄存器里的内容放到 rdi 寄存器, 也就是说接下来调用 string_length() 函数时,传入的第一个参数是 rbp 里的内容。 rbp 里现在是啥?
没错,在偏移地址7处, 是把 strings_not_equal() 第二个参数放到了 rbp 里, 现在作为 string_length 的第一个参数。紧接着的汇编代码是 call 0x40131b <string_length>
. 于是 C 代码更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2); // len2: eax
return 1;
}
mov edx,0x1
: 往寄存器 edx 里写入1。
cmp r12d,eax
: 比较 r12d 和 eax 两个寄存器的值。其中 eax 寄存器存放的是,最近一次函数调用(返回值是整数)的结果,也就是 string_length(s2)的结果; r12d 存放的是是调用 string_length(s1) 的结果, 是在偏移地址15 时存入的。 因此,现在这句汇编执行的是,s1 和 s2 两个字符串长度的比较。
jne 0x40139b <strings_not_equal+99>
: 如果比较的 s1 和 s2 的长度不相等, 那么跳转到偏移地址为99的汇编代码继续执行。 偏移地址99处的代码有分析过, 是把 edx
寄存器的值放入 eax
, 然后函数返回。而现在 edx
里存放的是1(偏移地址26时写入的)。因此C代码更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2); // len2: eax
if (len1 != len2)
{
return 1;
}
}
偏移地址 36, 39, 41, 80, 85, 99
0x000000000040135c <+36>: movzx eax,BYTE PTR [rbx]
0x000000000040135f <+39>: test al,al
0x0000000000401361 <+41>: je 0x401388 <strings_not_equal+80>
0x0000000000401388 <+80>: mov edx,0x0
0x000000000040138d <+85>: jmp 0x40139b <strings_not_equal+99>
0x000000000040139b <+99>: mov eax,edx
movzx eax,BYTE PTR [rbx]
: 把寄存器 rbx 里存放的值对应的内存地址处的值, 放到 eax 寄存器里, 并且注意 movzx 和 mov 有区别, movzx 是 move with zero extending 的意思,高位填充0。也就是说被放到 eax 的 [rbx]
, 是一个宽度小于 int 的内容. 对应的C代码:
char c1 = *(rbx);
// 也就是:
char c1 = *s1;
完整的 C 代码更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2); //
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
}
test al,al
: test 指令执行的是 AND 操作。 test al, al 意思是 al 和自己做 AND 操作。 test 指令不存储结果, 只修改 ZF, SF, PF。显然,如果 al 本身是0,那么 ZF 将等于1.
je 0x401388 <strings_not_equal+80>
: 如果 ZF 为1, 则跳转到偏移地址80的地方继续执行。 也就是说, 如果 al(eax的低8位,也就是刚刚C代码中的 c1)为0,那么就跳转到偏移地址80地方执行。 对应的 C 代码:
char c1 = *p1;
if (c1 == 0)
{
goto offset80;
}
地址偏移80,85, 99处的汇编:
mov edx, 0x0
: 往 edx 寄存器写入0。
jmp 0x40139b <strings_not_equal+99>
: 无条件跳转到 offset99。
mov eax, edx
: 把 edx 寄存器的值拷贝到 eax,也就是说 edx 里刚刚写入的0,是函数的返回值。
完整的C代码更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
return 0;
}
}
偏移地址 43, 46, 58, 62
是在偏移地址41的地方, ZF 不为0的情况下,继续执行的代码。也就是 char c1 = *s1
后, c1 != '\0'
情况下,继续执行的代码。
0x0000000000401363 <+43>: cmp al,BYTE PTR [rbp+0x0]
0x0000000000401366 <+46>: je 0x401372 <strings_not_equal+58>
0x0000000000401372 <+58>: add rbx,0x1
0x0000000000401376 <+62>: add rbp,0x1
cmp al, BYTE PTR [rbp+0x0]
: 比较两个值,第一个值是 al 寄存器, 也就是 C 代码中我们自行定义的 c1
变量; 第二个是 BYTE PTR [rbp+0x0]
, 也就是 rbp 寄存器中存储的值对应的内存地址处存放的值。
cmp 指令,可以理解为两个操作数做减法,如果结果等于0,那么ZF就更新为1,否则ZF更新为0.
je 0x401372 <strings_not_equal+58>
: 如果 ZF 为1(也就是cmp比较的两个操作数相等),那么跳转到偏移地址为58的地方。对应的C代码:
if (al == *rbp)
{
goto label58
}
也就是:
if (c1 == *p2)
{
goto label58
}
偏移地址为58的地方:
add rbx, 0x1
: rbx 寄存器加1. 也就是 rbx 更新为 rbx + 1。 对应的C代码:
p1++;
偏移地址为62的地方:
add rbp, 0x1
: 同上, rbp++:
p2++;
完整的C代码更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
return 0;
}
if (c1 == *p2)
{
p1++;
p2++;
}
}
偏移地址 66, 69, 71, 50, 53, 56, 94, 99
0x000000000040137a <+66>: movzx eax,BYTE PTR [rbx]
0x000000000040137d <+69>: test al,al
0x000000000040137f <+71>: jne 0x40136a <strings_not_equal+50>
0x000000000040136a <+50>: cmp al,BYTE PTR [rbp+0x0]
0x000000000040136d <+53>: nop DWORD PTR [rax]
0x0000000000401370 <+56>: jne 0x401396 <strings_not_equal+94>
0x0000000000401396 <+94>: mov edx,0x1
0x000000000040139b <+99>: mov eax,edx
movzx eax, BYTE PTR [rbx]
: 把 rbx 寄存器里存放的值对应的内存地址处, 存放到 eax 寄存器。感觉这里是把 eax 寄存器当作临时变量用了。实际上偏移地址36和66的汇编代码一样:
0x000000000040135c <+36>: movzx eax,BYTE PTR [rbx]
0x000000000040137a <+66>: movzx eax,BYTE PTR [rbx]
含义自然也是一样的:char c1 = *s1
. 只不过此时的 s1 已经是原始输入的 s1 再加 1 了。
test al, al
: 让 AL 和 AL 做 AND 操作, 如果结果为1, 则ZF为0. 如果 AND 结果为0, 则 ZF 为1.
jne 0x40136a <strings_not_equal+50>
: 检查 ZF, 如果ZF为1(也就是AL为0, 对应到C代码就是 c1 为 0),则跳转到偏移地址为50的地方继续执行。
对应的 C 代码
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
return 0;
}
if (c1 == *p2)
{
p1++;
p2++;
}
c1 = *p1;
if (c1 != '\0')
{
if (c1 != *p2)
{
return 1;
}
}
}
偏移地址 69, 71, 73, 78, 99
0x000000000040137d <+69>: test al,al
0x000000000040137f <+71>: jne 0x40136a <strings_not_equal+50>
0x0000000000401381 <+73>: mov edx,0x0
0x0000000000401386 <+78>: jmp 0x40139b <strings_not_equal+99>
0x000000000040139b <+99>: mov eax,edx
if (c1 == '\0')
{
return 0;
}
完整代码更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
return 0;
}
if (c1 == *p2)
{
p1++;
p2++;
}
c1 = *p1;
if (c1 != '\0')
{
if (c1 != *p2)
{
return 1;
}
}
else
{
return 0;
}
}
50, 53, 56, 58, 62
0x000000000040136a <+50>: cmp al,BYTE PTR [rbp+0x0]
0x000000000040136d <+53>: nop DWORD PTR [rax]
0x0000000000401370 <+56>: jne 0x401396 <strings_not_equal+94>
0x0000000000401372 <+58>: add rbx,0x1
0x0000000000401376 <+62>: add rbp,0x1
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
return 0;
}
if (c1 == *p2)
{
goto label58;
}
label58:
p1++;
p2++;
c1 = *p1;
if (c1 != '\0')
{
if (c1 != *p2)
{
return 1;
}
else
{
goto label58;
}
}
else
{
return 0;
}
}
43, 46, 48, 87, 92, 99
0x0000000000401363 <+43>: cmp al,BYTE PTR [rbp+0x0]
0x0000000000401366 <+46>: je 0x401372 <strings_not_equal+58>
0x0000000000401368 <+48>: jmp 0x40138f <strings_not_equal+87>
0x000000000040138f <+87>: mov edx,0x1
0x0000000000401394 <+92>: jmp 0x40139b <strings_not_equal+99>
0x000000000040139b <+99>: mov eax,edx
if (c1 != *p2)
{
return 1;
}
完整的C代码更新为:
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
return 0;
}
if (c1 == *p2)
{
goto label58;
}
else
{
return 1;
}
label58:
p1++;
p2++;
c1 = *p1;
if (c1 != '\0')
{
if (c1 != *p2)
{
return 1;
}
else
{
goto label58;
}
}
else
{
return 0;
}
}
整理代码
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
return 0;
}
if (c1 != *p2)
{
return 1;
}
label58:
p1++;
p2++;
c1 = *p1;
if (c1 == '\0')
{
return 0;
}
if (c1 != '\0')
{
if (c1 != *p2)
{
return 1;
}
else
{
goto label58;
}
}
}
int strings_not_equal(const char* s1, const char* s2)
{
const char* p1 = s1; // p1: rbx
const char* p2 = s2; // p2: rbp
int len1 = string_length(p1); // len1: r12d
int len2 = string_length(p2);
if (len1 != len2)
{
return 1;
}
int ret = 0;
for (; ;)
{
char c1 = *p1; // c1: eax
if (c1 == '\0') // s1[0] 为 '\0', 而 s1, s2 长度相等, 说明 s2[0] 也是 '\0'
{
ret = 0;
break;
}
if (c1 != *p2)
{
ret = 1;
break;
}
p1++;
p2++;
}
return ret;
}