汇编语言之 Call, jmp, ret, retf

7010阅读 0评论2014-06-27 abin9630
分类:LINUX

对于这些个指令的细究,也是在看ULK中的 switch_to的宏实现时候

在切换进程A到进程B,调用__switch_to函数时,不直接使用 call,而是采用先 push 进程B的eip,然后调用 jmp,最后在__switch_to中返回时候通过 ret指令来切换到 进程B中eip标记位置的指令来执行(以上是题外话)

对于 call和 jmp,都可以进行完成指令的跳转目的,区别在于
call指令相当于执行了:
1. push eip
2. jmp
比单单的 jmp多一步 push eip的动作

实际上,无论是 call还是 jmp,都会有短跳和长跳的区别
jmp: 无论是长跳还是短跳,不会有任何区别,而仅仅在于目的不同而已。长跳对应于段间,短跳则是在段内
call:长跳和短跳是有区别的,这个对应于后续的 ret和 retf指令。同上所述,call指令在执行前会进行push eip的动作。实际上,如果是长跳,在 push eip之前,会有 push cs的动作,会将代码段寄存器同时压栈;如果是短跳,仅仅只对 eip压栈(对于长跳,记作 call far ptr标号;对于短跳,记作 call near ptr标号)

对应的,ret和 retf命令就和call是遥相呼应之势
ret指令,在返回时候,会有 pop eip的动作
而对于 retf指令,在执行返回时候,首先会 pop cs,然后 pop eip,最后才真正返回到调用前 eip指向的指令位置开始执行

所以,对于
call far ptr1
在 ptr1执行完返回时,应该使用 retf来返回。
对于
call near ptr2
在 ptr2执行完返回时,应该使用 ret来返回。

而对于
jmp ptr3
理论上,在 ptr3执行完后,应该也使用 jmp来跳回到原先的位置来执行,而不能直接使用 ret,更不能直接使用 retf

比如

点击(此处)折叠或打开

  1. .ptr2:
  2.     push %ebp;
  3.     movl %esp, %ebp;
  4. ...
  5. ...
  6.     jmp ptr3;
  7. // End of .ptr2
  8. ...
  9. .ptr1:
  10.     jmp ptr2;
  11. .ptr3:
  12.     mov %edx, %ebx;
  13. ...
  14. ...
  15. ...
但是实际上,如果按照上述的实现,在 ptr2中使用 jmp ptr3来跳回到 ptr3指令继续执行,或许会隐含有一个你不希望的结果:
使用 jmp ptr3跳回后,不会再通过 eax寄存器来返回被调用函数的返回值

而这就涉及到我们将要讨论的第二点,函数的参数和返回值
对于函数参数的传递和返回值,
一种是直接通过寄存器来传递
一种是通过栈数据来进行传递

我们知道,cpu的寄存器的个数是有限制的,而对于函数的参数,理论上可以有多个(至少可以有超过8个);而对于返回值,不仅可以返回一个 int数据,同时也可以返回一个大型结构体数据(在这种情形下,就不能可能使用有限的寄存器来完成返回值的传递)

对于函数参数,寄存器和栈数据都会被使用到用来实现参数传递动作。
如果对于参数数量较多的情形下
1. 寄存器中不会存放所有的参数,(由于寄存器的复用,仅仅会保存有限个参数)(通常 eax保存第一个参数,edx保存第二个参数)
2. 栈数据中存放的是所有的参数

对于函数的返回值
1. 对于 int型数据,直接使用 eax返回函数返回值
2. 如果是大型结构体数据,会使用栈来传递

因此,对于上述的代码中,在使用 “jmp ptr2"后,保存在 eax和 edx等等寄存器中的函数参数是没有问题的
但是如果在 ptr2中使用 jmp ptr3跳回到 ptr3继续执行时, eax中保存的不在是 ptr2中你想返回的那个函数值。

比如对于以下函数

点击(此处)折叠或打开

  1. #include <string.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>

  4. int
  5. mul3(int ia)
  6. {
  7.     int ib = 0, ret = 0;
  8.     __asm__(
  9.             "movl %%eax, %0;"
  10.             "movl %2, %%eax;"
  11.             "addl %2, %%eax;"
  12.             "addl %2, %%eax;"
  13.             "movl %%eax, %1;"
  14.             :"=m" (ib), "=m" (ret)
  15.             :"m" (ia)
  16.            );

  17.     printf("%s, eax: %x\n", __FUNCTION__, ib);

  18.     return ret;
  19. }

  20. int
  21. main(void)
  22. {
  23.     int ia = 4, ret = 0;

  24.     __asm__(
  25.             "movl %%eax, %0;"
  26.             :"=m" (ret)
  27.             :
  28.            );

  29.     printf("%s, Before eax: %x\n", __FUNCTION__, ret);

  30.     ret = mul3(ia);

  31.     __asm__(
  32.             "movl %%eax, %0;"
  33.             :"=m" (ia)
  34.             :
  35.            );
  36.     printf("%s, After mul3, ret: %x, eax: %x\n", __FUNCTION__, ret, ia);
  37. }

其产生的汇编代码为:

点击(此处)折叠或打开

  1. [martin@assemble]$ cat call_ret.s
  2.     .file    "call_ret.c"
  3.     .section    .rodata
  4. .LC0:
  5.     .string    "%s, eax: %x\n"
  6.     .text
  7.     .globl    mul3
  8.     .type    mul3, @function
  9. mul3:
  10. .LFB2:
  11.     .cfi_startproc
  12.     pushl    %ebp
  13.     .cfi_def_cfa_offset 8
  14.     .cfi_offset 5, -8
  15.     movl    %esp, %ebp
  16.     .cfi_def_cfa_register 5
  17.     subl    $40, %esp
  18.     movl    $0, -16(%ebp)
  19.     movl    $0, -12(%ebp)
  20. #APP
  21. # 9 "call_ret.c" 1
  22.     movl %eax, -16(%ebp);movl 8(%ebp), %eax;addl 8(%ebp), %eax;addl 8(%ebp), %eax;movl %eax, -12(%ebp);
  23. # 0 "" 2
  24. #NO_APP
  25.     movl    -16(%ebp), %eax
  26.     movl    %eax, 8(%esp)
  27.     movl    $__FUNCTION__.2508, 4(%esp)
  28.     movl    $.LC0, (%esp)
  29.     call    printf
  30.     movl    -12(%ebp), %eax
  31.     leave
  32.     .cfi_restore 5
  33.     .cfi_def_cfa 4, 4
  34.     ret
  35.     .cfi_endproc
  36. .LFE2:
  37.     .size    mul3, .-mul3
  38.     .section    .rodata
  39. .LC1:
  40.     .string    "%s, Before eax: %x\n"
  41.     .align 4
  42. .LC2:
  43.     .string    "%s, After mul3, ret: %x, eax: %x\n"
  44.     .text
  45.     .globl    main
  46.     .type    main, @function
  47. main:
  48. .LFB3:
  49.     .cfi_startproc
  50.     pushl    %ebp
  51.     .cfi_def_cfa_offset 8
  52.     .cfi_offset 5, -8
  53.     movl    %esp, %ebp
  54.     .cfi_def_cfa_register 5
  55.     andl    $-16, %esp
  56.     subl    $32, %esp
  57.     movl    $4, 24(%esp)
  58.     movl    $0, 28(%esp)
  59. #APP
  60. # 29 "call_ret.c" 1
  61.     movl %eax, 28(%esp);
  62. # 0 "" 2
  63. #NO_APP
  64.     movl    28(%esp), %eax
  65.     movl    %eax, 8(%esp)
  66.     movl    $__FUNCTION__.2514, 4(%esp)
  67.     movl    $.LC1, (%esp)
  68.     call    printf
  69.     movl    24(%esp), %eax
  70.     movl    %eax, (%esp)
  71.     call    mul3
  72.     movl    %eax, 28(%esp)
  73. #APP
  74. # 39 "call_ret.c" 1
  75.     movl %eax, 24(%esp);
  76. # 0 "" 2
  77. #NO_APP
  78.     movl    24(%esp), %edx
  79.     movl    28(%esp), %eax
  80.     movl    %edx, 12(%esp)
  81.     movl    %eax, 8(%esp)
  82.     movl    $__FUNCTION__.2514, 4(%esp)
  83.     movl    $.LC2, (%esp)
  84.     call    printf
  85.     leave
  86.     .cfi_restore 5
  87.     .cfi_def_cfa 4, 4
  88.     ret
  89.     .cfi_endproc
  90. .LFE3:
  91.     .size    main, .-main
  92.     .section    .rodata
  93.     .type    __FUNCTION__.2508, @object
  94.     .size    __FUNCTION__.2508, 5
  95. __FUNCTION__.2508:
  96.     .string    "mul3"
  97.     .type    __FUNCTION__.2514, @object
  98.     .size    __FUNCTION__.2514, 5
  99. __FUNCTION__.2514:
  100.     .string    "main"
  101.     .ident    "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
  102.     .section    .note.GNU-stack,"",@progbits
  103. [martin@assemble]$
从中可以看到,在 main中调用 mul3函数时候使用的是 call(71行所示),而不是 jmp
同样在 mul3函数执行完后,使用ret指令来返回(34行所示),而不是直接使用jmp方式

该函数中实现:
1. 在main中
1.1 调用 mul3之前,打印 eax的值来确认后续在调用 mul3后,eax有被修改成参数值
1.2 在调用 mul3后,打印 ret(函数的返回值)和  eax的值,来对比两者是否相同
2. 在 mul3中
2.1 首先打印 eax的值确认是否已经被改编成参数值
2.2 然后在计算后,保存 eax的值到 ret后,返回 ret

该函数执行结果:

点击(此处)折叠或打开

  1. [martin@assemble]$ ./a.out
  2. main, Before eax: 1
  3. mul3, eax: 4
  4. main, After mul3, ret: c, eax: c
  5. [martin@assemble]$
可以看到,如我们所预期
1. 到 mul3函数后, eax的值被设置成参数值 4
2. mul3返回后, eax的值和 ret的值相同,都是 12

现在如果我们来做少许改动:
1. 将71行的 call mul3该成 jmp mul3(该处改完后,需要同步修改下面第2步骤后,在编译执行,否则会出现segment fault,段错误。因为ret指令返回后,pop eip时候将不再能获取到 72行的指令地址,因为现在使用了 jmp,而不是call,eip没有被压栈)
2. 在 mul3中的34行,使用 jmp .Martin来替换 ret(在 72行添加 .Martin的标记,以便在 mul3中可以直接 jmp过来)。

修改后的汇编源码如下:

点击(此处)折叠或打开

  1. [martin@assemble]$ cat call_ret.s
  2.     .file    "call_ret.c"
  3.     .section    .rodata
  4. .LC0:
  5.     .string    "%s, eax: %x\n"
  6.     .text
  7.     .globl    mul3
  8.     .type    mul3, @function
  9. mul3:
  10. .LFB2:
  11.     .cfi_startproc
  12.     pushl    %ebp
  13.     .cfi_def_cfa_offset 8
  14.     .cfi_offset 5, -8
  15.     movl    %esp, %ebp
  16.     .cfi_def_cfa_register 5
  17.     subl    $40, %esp
  18.     movl    $0, -16(%ebp)
  19.     movl    $0, -12(%ebp)
  20. #APP
  21. # 9 "call_ret.c" 1
  22.     movl %eax, -16(%ebp);movl 8(%ebp), %eax;addl 8(%ebp), %eax;addl 8(%ebp), %eax;movl %eax, -12(%ebp);
  23. # 0 "" 2
  24. #NO_APP
  25.     movl    -16(%ebp), %eax
  26.     movl    %eax, 8(%esp)
  27.     movl    $__FUNCTION__.2508, 4(%esp)
  28.     movl    $.LC0, (%esp)
  29.     call    printf
  30.     movl    -12(%ebp), %eax
  31.     leave
  32.     .cfi_restore 5
  33.     .cfi_def_cfa 4, 4
  34.     jmp .Martin
  35.     .cfi_endproc
  36. .LFE2:
  37.     .size    mul3, .-mul3
  38.     .section    .rodata
  39. .LC1:
  40.     .string    "%s, Before eax: %x\n"
  41.     .align 4
  42. .LC2:
  43.     .string    "%s, After mul3, ret: %x, eax: %x\n"
  44.     .text
  45.     .globl    main
  46.     .type    main, @function
  47. main:
  48. .LFB3:
  49.     .cfi_startproc
  50.     pushl    %ebp
  51.     .cfi_def_cfa_offset 8
  52.     .cfi_offset 5, -8
  53.     movl    %esp, %ebp
  54.     .cfi_def_cfa_register 5
  55.     andl    $-16, %esp
  56.     subl    $32, %esp
  57.     movl    $4, 24(%esp)
  58.     movl    $0, 28(%esp)
  59. #APP
  60. # 29 "call_ret.c" 1
  61.     movl %eax, 28(%esp);
  62. # 0 "" 2
  63. #NO_APP
  64.     movl    28(%esp), %eax
  65.     movl    %eax, 8(%esp)
  66.     movl    $__FUNCTION__.2514, 4(%esp)
  67.     movl    $.LC1, (%esp)
  68.     call    printf
  69.     movl    24(%esp), %eax
  70.     movl    %eax, (%esp)
  71.     jmp mul3
  72. .Martin:
  73.     movl    %eax, 28(%esp)
  74. #APP
  75. # 39 "call_ret.c" 1
  76.     movl %eax, 24(%esp);
  77. # 0 "" 2
  78. #NO_APP
  79.     movl    24(%esp), %edx
  80.     movl    28(%esp), %eax
  81.     movl    %edx, 12(%esp)
  82.     movl    %eax, 8(%esp)
  83.     movl    $__FUNCTION__.2514, 4(%esp)
  84.     movl    $.LC2, (%esp)
  85.     call    printf
  86.     leave
  87.     .cfi_restore 5
  88.     .cfi_def_cfa 4, 4
  89.     ret
  90.     .cfi_endproc
  91. .LFE3:
  92.     .size    main, .-main
  93.     .section    .rodata
  94.     .type    __FUNCTION__.2508, @object
  95.     .size    __FUNCTION__.2508, 5
  96. __FUNCTION__.2508:
  97.     .string    "mul3"
  98.     .type    __FUNCTION__.2514, @object
  99.     .size    __FUNCTION__.2514, 5
  100. __FUNCTION__.2514:
  101.     .string    "main"
  102.     .ident    "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
  103.     .section    .note.GNU-stack,"",@progbits
  104. [martin@assemble]$
该函数执行后,结果是

点击(此处)折叠或打开

  1. [martin@assemble]$ ./a.out
  2. main, Before eax: 1
  3. mul3, eax: 4
  4. main, After mul3, ret: 180d9131, eax: 180d9131
  5. [martin@assemble]$
可以看到
1. mul3 中 eax代表的参数值依旧是 4,这个没有问题
2. main中调用 mul3后的返回值
2.1 虽然现在 ret和 eax的值依旧是相同的
2.2 但是看得出来,这个结果并非是 mul3的计算结果12,而是一个随机的垃圾值,无任何意义

从以上可以看得出来
1. 通常我们需要配对使用 call和 ret/retf
2. 如果在特殊情况下,需要配对使用 jmp和 ret。那么千万注意,在 jmp fun1之前,应该 push一个你希望在fun1返回时候转去执行的指令的地址(某eip值)。当然这种情况比较少见,但是 kernel的 switch_to就是这样的一个例子

最后说明下,在汇编中如果直接使用 eip寄存器,会提示错误信息。 Error: bad register name `%eip'

点击(此处)折叠或打开

  1. [martin@assemble]$ gcc call_ret.s
  2. call_ret.s: Assembler messages:
  3. call_ret.s:70: Error: bad register name `%eip

如果你想查看 eip的值,可以通过先调用call,然后 pop ebx的方式,然后查看 ebx的值。因为这样,ebx中 pop出来的就是 call指令 push进去的 eip的值

点击(此处)折叠或打开

  1. ...
  2. call .TestPop
  3. . TestPop
  4. pop %ebx
  5. ...


---------------------------------------参考内容----------------------------------------
跳转指令 jmp、call、ret、retf
汇编语言--call和ret指令

上一篇:【内核】进程切换 switch_to 与 __switch_to
下一篇:ebp, esp, eip相关寄存器简介