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 寄存器内容压栈。对应的是 +103pop 103, 是从栈上恢复 r12 寄存器。
为啥要压栈和恢复?因为 r12 寄存器要被当前函数 strings_not_equal 使用, 而调用规约规定, callee 要在函数结束时恢复 r12, 这样的话,前面一个调用 r12 的函数(frame)能继续用 r12。

+2: push rbp: 将基指针寄存器(Base Pointer,rbp)的内容压入栈中。rbp 通常用于标记当前栈帧的开始,它在函数调用过程中用于定位局部变量和函数参数。
显然, 先压入 r12, 再压入 rbp 是比较合理的。
对应的是 +102pop rbp, 弹出 rbp。

+3 push rbx: 将 rbx 寄存器的内容压入栈中。rbx 是另一个被调用者保存寄存器,它在函数执行过程中用于保存一个临时值,或者作为一个指向数据的指针。
对应的是 +101pop 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;
}
01-13 11:59