堆栈指针和程序计数器有什么区别? [英] What is the difference between Stack Pointer and Program Counter?

查看:124
本文介绍了堆栈指针和程序计数器有什么区别?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

众所周知,微处理器执行任务的过程只是从存储器一个接一个地执行二进制指令,并且有一个程序计数器保存下一条指令的地址.因此,如果我没有记错的话,这就是处理器执行任务的方式.但是还有另一个名为堆栈指针的指针,它的作用与程序计数器几乎相同.我的问题是为什么我们需要一个堆栈指针来指向内存(堆栈)的地址?有人可以告诉我堆栈指针和程序计数器之间的主要区别吗?

解决方案

  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,#12010:e92d4010 push {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 orr r0,r4,#12034:e8bd4010 pop {r4,lr}2038:e12fff1e bx lr 

采用简单的功能,在您为这个问题标记手臂时,使用arm指令集之一进行编译和反汇编.

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

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

第一条指令测试r0的lsbit,传入参数(x),程序计数器未修改,因此下一次读取操作将从0x2010读取0xe92d4010

程序计数器现在包含0x2014,执行0x2010指令.该指令是推送,它使用堆栈指针.作为程序员进入此函数时,我们不在乎堆栈指针的确切值是0x2468还是0x4010,我们不在乎.因此,我们只说它包含值/地址sp_start.此推指令使用堆栈来保存两件事,一是链接寄存器lr,r14,即返回地址,当此函数完成时,我们要返回到调用函数.根据该编译器对此指令集使用的调用约定的规则,r4表示必须保留r4,因为如果对其进行修改,则必须将其返回到其被调用时的值.因此,我们将其保存在堆栈中,而不是将x放在堆栈中并在此函数中引用xa次,该编译器选择保存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进行提取

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

从0x2020获取,将PC更改为0x2024,执行弹出.我们尚未修改堆栈指针(保留了另一个寄存器,您必须将其放回找到它的位置),因此现在我们从sp_start-8读取并等于sp_start-8(即sp + 0).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指令不会修改程序计数器.

获取0x2034,将pc设置为0x2038,执行0x2034,就像0x2020一样,将值取到地址sp + 0并将其放入r4取sp + 4并将其放入lr,然后将8加到堆栈指针.

获取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将用于被调用方函数知道可用空间在哪里开始.帧指针通常会浪费寄存器,但是某些实现默认情况下会使用它,并且有些指令集是您无法选择的,要达到两倍的指令集,它们将需要使用特定寄存器对堆栈访问进行硬编码,并且偏移仅在一个方向上添加一个正偏移或一个负偏移.在这种情况下,推入实际上是针对通用存储倍数的伪指令,在其中对寄存器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.

这篇关于堆栈指针和程序计数器有什么区别?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆