最近看操作系统的代码,发现了利用pop %esp进行用户栈和内核栈之间的跳转,感觉挺有意思的,就研究了一下。其实这个问题本来不复杂,指令的设计方式蛮合理的,但是看了一下Intel的手册关于pop和push的描述反而把我弄晕了。
最后又仔细看了一下关于pop %esp和push %ebp的描述才明白,其实就是我一开始没看手册的时候想的那样。。。把intel手册上面对普通的pop和push的描述往push %esp和pop %esp上面套是我被弄晕的根本原因。(下面的叙述都基于x86-32)
普通的popl DEST:
DEST ← SS:ESP;
ESP ← ESP + 4普通的pushl SRC
ESP ← ESP – 4;
Memory[SS:ESP] ← SRC
上面两个规则非常显然,根本不用解释,但是当你把DEST和SRC换成esp的时候,规则其实是不成立的。关于操作数是esp的情况,Intel手册上有特殊说明:
The POP ESP instruction increments the stack pointer (ESP) before data at the old top of stack is written into the destination.
The PUSH ESP instruction pushes the value of the ESP register as it existed before the instruction was executed. If a PUSH instruction uses a memory operand in which the ESP register is used for computing the operand address, the address of the operand is computed before the ESP register is decremented.
英文不好我就不翻译了,我觉得可以简单理解如下:
popl %esp=movl (%esp),%esp
pushl %esp=subl $4,%esp; leal 4(%esp),%eax; movl %eax,(%esp)**
这样一来,pop esp实现的是栈顶的跳转,push esp则相当于把新的栈顶变成了指向原来栈顶的指针。
另外还可以提一下pusha和popl指令,这两个指令分别把8个通用寄存器依次压栈和弹栈。其中特殊的还是esp寄存器。
对于pusha指令,相当于做了如下操作:
Temp ← (ESP);
Push(EAX);
Push(ECX);
Push(EDX);
Push(EBX);
Push(Temp);
Push(EBP);
Push(ESI);
Push(EDI);
需要注意的是轮到esp时,被压入的是pusha指令之前的栈顶地址。
对于popa指令,相当于做了如下操作:
EDI ← Pop();
ESI ← Pop();
EBP ← Pop();
Increment ESP by 4; (* Skip next 4 bytes of stack *)
EBX ← Pop();
EDX ← Pop();
ECX ← Pop();
EAX ← Pop();
需要注意的是轮到esp时,只是单纯的栈顶+4,对应内存里的数值并不会被弹出到esp中,否则地址会跳转。
下面写一段测试代码(AT&T汇编)验证一下:
64位环境下汇编和链接命令如下:
as -o push_pop_test.o push_pop_