且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

堆栈指针和程序计数器有什么区别?

更新时间:2023-11-06 11:45:28

void show ( unsigned int );unsigned int fun ( unsigned int x ){if(x&1) 显示(x+1);返回(x|1);}0000200c<乐趣>:200c:e3100001 tst r0,#12010 年:e92d4010 推送 {r4, lr}2014 年:e1a04000 mov r4、r02018 年:1a000002 bne 2028 <fun+0x1c>201c: e3840001 orr r0, r4, #12020 年:e8bd4010 pop {r4, lr}2024:e12fff1e bx lr2028 年:e2800001 添加 r0、r0、#1202c:ebfffff5 bl 2008 <show>2030 年:e3840001 或 r0、r4、#12034: e8bd4010 pop {r4, lr}2038: e12fff1e bx lr

当你在这个问题上标记 arm 时,使用一个简单的函数,使用 arm 指令集之一编译和反汇编.

让我们假设一个简单的串行非管道老式执行.

为了到达这里,发生了一个调用(在此指令集中,分支和链接中的 bl)将程序计数器修改为 0x200C.程序计数器用于获取该指令 0xe3100001,然后在获取之后在执行之前将程序计数器设置为指向下一条指令 0x2010.由于此程序计数器是针对此特定指令集描述的,因此它会提取并暂存下一条指令 0xe92d4010,并且在执行 0x200C 指令之前,pc 包含值 0x2014,即前面两条指令.出于演示目的,让我们想想我们从 0x200C 获取 0xe3100001 的老派,现在将 pc 设置为 0x2010 等待执行完成和下一个获取周期.

第一条指令测试r0的lsbit,传入的参数(x),程序计数器没有被修改,所以下一次fetch从0x2010读取0xe92d4010

程序计数器现在包含 0x2014,执行 0x2010 指令.该指令是使用堆栈指针的压入.作为程序员进入这个函数时,我们不关心堆栈指针的确切值是多少,可能是 0x2468,也可能是 0x4010,我们不在乎.所以我们只会说它包含值/地址 sp_start.这个push指令是用栈来保存两件事的,一个是链接寄存器lr,r14,返回地址,当这个函数执行完我们要返回调用函数.并且 r4 根据此编译器为此指令集使用的调用约定的规则,必须保留 r4,因为如果您修改它,则必须将其返回到调用时的值.所以我们将把它保存在堆栈上,而不是把 x 放在堆栈上并在这个函数中多次引用 x,这个编译器选择保存 r4 中的任何内容(我们不在乎我们只需要保存它)和使用 r4 在此函数的编译期间保持 x .我们调用的任何函数和他们调用的函数等都会保留 r4,因此当我们调用的任何人返回给我们时,r4 就是我们调用时的任何内容.因此堆栈指针本身更改为 sp_start-8 并且在 sp_start-8 处保存 r4 的保存副本,在 sp_start-4 处保存 lr 或 r14 的副本,我们现在可以修改 r4 或 lr,因为我们希望我们有一个便笺簿(堆栈) 带有一个保存的副本和一个指针,我们可以对其进行相对寻址以获取这些值,并且任何想要使用堆栈的调用函数将从 sp_start-8 向下增长,而不是踩在我们的便笺上.>

现在我们获取 0x2014 将 pc 更改为 0x2018,这会在 r4 中创建 x(在 r0 中传入)的副本,以便我们稍后在函数中使用它.

我们获取 0x2018 将 pc 更改为 0x201C.这是一个条件分支,因此根据条件,PC 将保持 0x201C 或更改为 0x2028.有问题的标志是在执行 tst r0,#1 期间设置的,其他指令没有触及该标志.所以我们现在有两条路径要走,如果条件不成立,那么我们使用 0x201C 来获取

fetch from 0x201c 将 pc 更改为 0x2020,这将执行 x=x|1,r0 是包含函数返回值的寄存器.该指令不修改程序计数器

fetch from 0x2020 将pc改为0x2024,执行pop.我们没有修改堆栈指针(另一个被保留的寄存器,你必须把它放回你找到它的地方)所以 sp 等于 sp_start-8(即 sp+0)现在我们从 sp_start-8 读取并放入r4 中的该值,从 sp_start-4(即 sp+4)中读取并将该值放入 lr 并将 8 添加到堆栈指针,因此它现在设置为 sp_start,即我们启动时的值,将其放回你找到它的方式.

从 0x2024 获取将 pc 更改为 0x2028.bx lr 是到 r14 的分支,基本上它是函数的返回,这会修改程序计数器以指向调用函数,调用函数之后的指令称为 fun().pc 被修改后继续执行该函数.

如果 0x2018 处的 bne 确实发生了,那么在 bne 执行期间 pc 更改为 0x2028,我们从 0x2028 获取并在执行前将 pc 更改为 0x202c.0x2028 为加法指令,不修改程序计数器.

我们从 0x202c 获取并在执行前将 pc 更改为 0x2030.bl 指令确实修改了程序计数器和链接寄存器,它在这种情况下将链接寄存器设置为 0x2030,将程序计数器设置为 0x2008.

show 函数执行并返回 0x2030 的提取,将 pc 更改为 0x2034 发生在 0x2030 的 orr 指令不会修改程序计数器

fetch 0x2034 set pc to 0x2038 execute 0x2034, like 0x2020 this take the value at address sp+0 and put it in r4 take sp+4 and put it in the lr 然后将8添加到堆栈指针.

fetch 0x2038 将 pc 设置为 0x203c.这会返回将调用者的返回地址放入程序计数器中,从而导致下一次提取来自该地址.

程序计数器用于获取当前指令并指向下一条指令.

在这种情况下,堆栈指针执行两项工作,它显示堆栈顶部的位置,可用空间的开始位置以及提供访问此函数中项目的相对地址,因此在此函数的持续时间内将保存的 r4 寄存器推入 sp+0,因为此代码是设计的,返回地址位于 sp+8.如果我们在堆栈上还有其他几个东西,那么堆栈指针将被进一步移动到当时的可用空间中,堆栈上的项目将位于 sp+0、sp+4、sp+8 等或其他值为 8 的值、16、32 或 64 位项.

一些指令集和一些编译器设置也可以设置一个帧指针,它是第二个堆栈指针.一项工作是跟踪已用堆栈空间和可用堆栈空间之间的边界.另一项工作是提供一个指针,从中进行相对寻址.在此示例中,堆栈指针本身 r13 用于两个作业.但是我们可以告诉编译器,在其他指令集中,您别无选择,我们可以将帧指针保存到堆栈中,然后帧指针 = 堆栈指针.然后我们在这种情况下将堆栈指针移动 8 个字节,帧指针将用作 fp-4 和 fp-8 可以说是寻址堆栈上的两个项目,sp 将用于被调用函数以了解可用空间的位置开始.帧指针通常会浪费寄存器,但有些实现默认使用它,并且有些指令集您没有选择,要达到两倍,它们将需要使用特定寄存器对堆栈访问进行硬编码,并且仅在一个方向上的偏移添加正偏移或负偏移.在这种情况下,在 arm 中,推送实际上是一个用于对寄存器 r13 进行编码的通用存储倍数的伪指令.

有些指令集您看不到它以任何方式都看不到的程序计数器.同样,某些指令集您看不到堆栈指针,它以任何方式都看不到.

As we always know the procedure of executing task by a microprocessor is just executing binary instructions from memory one by one and there is a program counter which holds the address of the next instruction. So this is how processor executes it's tasks if I am not wrong. But there is also another pointer named Stack Pointer which does almost same thing like the program counter. My question is why we need a Stack Pointer to point address of memory(Stack)? Can somebody tell me about the main difference between Stack Pointer and program counter?

void show ( unsigned int );
unsigned int fun ( unsigned int x )
{
    if(x&1) show(x+1);
    return(x|1);
}

0000200c <fun>:
    200c:   e3100001    tst r0, #1
    2010:   e92d4010    push    {r4, lr}
    2014:   e1a04000    mov r4, r0
    2018:   1a000002    bne 2028 <fun+0x1c>
    201c:   e3840001    orr r0, r4, #1
    2020:   e8bd4010    pop {r4, lr}
    2024:   e12fff1e    bx  lr
    2028:   e2800001    add r0, r0, #1
    202c:   ebfffff5    bl  2008 <show>
    2030:   e3840001    orr r0, r4, #1
    2034:   e8bd4010    pop {r4, lr}
    2038:   e12fff1e    bx  lr

take a simple function, compile and disassemble using one of the arm instruction sets as you tagged arm on this question.

Lets assume a simple serial non-pipe old school type execution.

In order to get here a call (bl in this instruction set, branch and link) happened which modified the program counter to be 0x200C. The program counter is used to fetch that instruction 0xe3100001 then after fetch before execution the program counter is set to point at the next instruction 0x2010. As this program counter is described for this particular instruction set it fetches and stages the next instruction 0xe92d4010 and before execution of the 0x200C instruction the pc contains the value 0x2014, two instructions ahead. For demonstration purposes lets think old school we fetched 0xe3100001 from 0x200C the pc is now set to 0x2010 waiting for execution to complete and for the next fetch cycle.

This first instruction tests the lsbit of r0, the passed in parameter (x), the program counter is not modified so the next fetch reads 0xe92d4010 from 0x2010

The program counter now contains 0x2014, the 0x2010 instruction executes. This instruction is a push it uses the stack pointer. On entry into this function as a programmer we dont care what the exact value of the stack pointer is it could be 0x2468 it could be 0x4010, we dont care. So we will just say it contains the value/address sp_start. This push instruction is using the stack to save two things one is the link register lr, r14, the return address, when this function finishes we want to return to the calling function. And r4 which per the rules of the calling convention used by this compiler for this instruction set says that r4 must be preserved in that if you modify it you must return it to the value it was when called. So we are going to save that on the stack, instead of putting x on the stack and referring to x a number of times in this function, this compiler chooses to save whatever was in r4 (we dont care we just have to save it) and use r4 to hold x for the duration of this function as compiled. any function we call and they call, etc will preserve r4 so when anyone we call returns back to us r4 is whatever it was when we called. So the stack pointer itself changes to sp_start-8 and at sp_start-8 lives the saved copy of r4 and at sp_start-4 the saved copy of lr or r14, we can now modify r4 or lr as we wish we have a scratch pad (the stack) with a saved copy and a pointer for which we can do relative addressing to get at those values, and any calling functions that want to use the stack will grow down from sp_start-8 and not stomp on our scratch pad.

Now we fetch 0x2014 change the pc to 0x2018, this makes a copy of x (passed in in r0) in r4 so we can use it later in the function.

we fetch 0x2018 change the pc to 0x201C. This is a conditional branch so depending on the condition the pc will remain 0x201C or it will change to 0x2028. The flag in question was set during execution of tst r0,#1 the other instructions didnt touch that flag. So we have two paths to follow now, if the condition is not true then we use 0x201C to fetch

fetch from 0x201c change pc to 0x2020, this performs the x=x|1, r0 is the register that contains the return value for the function. This instruction does not modify the program counter

fetch from 0x2020 change the pc to 0x2024, execute the pop. we have not modified the stack pointer (another register that is preserved, you have to put it back where you found it) so sp is equal to sp_start-8 (which is sp+0) right now we read from sp_start-8 and put that value in r4, read from sp_start-4 (which is sp+4) and put that value in lr and add 8 to the stack pointer so it is now set to sp_start, the value it was when we started, put it back the way you found it.

fetch from 0x2024 change the pc to 0x2028. bx lr is a branch to r14 basically it is a return from the function, this modifies the program counter to point at the calling function, the instruction after that calling function called fun(). pc is modified execution continues from that function.

If the bne at 0x2018 did happen then the pc during the execution of bne changes to 0x2028 we fetch from 0x2028 and change the pc to 0x202c before execution. 0x2028 is an add instruction, does not modify the program counter.

we fetch from 0x202c and change the pc to 0x2030 before executing. the bl instruction does modify the program counter and the link register it sets the link register to 0x2030 in this case and the program counter to 0x2008.

the show function executes and returns with a fetch of 0x2030 changing the pc to 0x2034 the orr instruction at 0x2030 happens does not modify the program counter

fetch 0x2034 set pc to 0x2038 execute 0x2034, like 0x2020 this takes the value at address sp+0 and puts it in r4 takes sp+4 and puts it in the lr and then adds 8 to the stack pointer.

fetch 0x2038 set the pc to 0x203c. this does a return puts the callers return address in the program counter causing the next fetch to be from that address.

The program counter is used to fetch the current instruction and to point at the next instruction.

The stack pointer in this case does both jobs it shows where the top of stack is, where the free to use space starts as well as provides a relative address to access items in this function so for the duration of this function after the push the saved r4 register is at sp+0 as this code is designed and the return address at sp+8. if we had several other things on the stack then the stack pointer would have been moved further into the then free space and the items on the stack would be at sp+0, sp+4, sp+8, etc or other values for 8, 16, 32 or 64, bit items.

Some instruction sets and some compiler settings can also setup a frame pointer which is a second stack pointer. One job is to keep track of the boundary between used stack space and free stack space. The other job is to provide a pointer from which to do relative addressing. In this example the stack pointer itself r13 was used for both jobs. But we could tell the compiler and in other instruction sets you dont have a choice, we could have the frame pointer get saved to the stack then frame pointer = stack pointer. then we move the stack pointer in this case 8 bytes and the frame pointer would be used as fp-4 and fp-8 lets say to address the two items on the stack and sp would be used for callee functions to know where the free space starts. A frame pointer is generally a waste of a register, but some implementations use it by default and there are some instruction sets that you dont have a choice, to reach twice as far they will require stack accesses to be hardcoded using a specific register and the offset to be in only one direction add a positive offset or a negative. In arm in this case the push is actually a pseudo instruction for a generic store multiple in which the register r13 is encoded.

Some instruction sets you cant see the program counter it isnt visible to you in any way. Likewise some instruction sets you cannot see the stack pointer it is not visible to you in any way.