19

This has always confused me. Why can you not directly obtain the IP, and instead have to go through some odd assembly hoops such as calling a function whose only purpose is to push its own return address onto the stack?

I'm asking about the historical reason, since this decision was probably made back in the time of the 8086.

10
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Chenmunka
    May 21, 2022 at 7:52
  • 12
    Worth noting is that x86-64 does have such an instruction: lea eax,[eip+0]
    – jpa
    May 21, 2022 at 9:12
  • 1
    BTW, there are plenty of other CPUs where you cannot do that directly, either.
    – dirkt
    May 21, 2022 at 11:33
  • 2
    The problem with asking why is that only the engineers that made the decisions really can answer. The rest of us can make guesses: they could not at the time think of any use for it, it would take too many transistors or slow down something, they might be planning for the future with more adress bits or modifiers, they knew of a good workaround, the marketing people did not require it, programmers managed anyway, it might create other problems somewhere, ...
    – ghellquist
    May 21, 2022 at 16:37
  • 8
    @ghellquist Many such engineers are still alive. Engineers can be and have been interviewed. Some engineers are interested in the retro scene. Engineers must not bee seen as 100% opaque and inaccessible godlike figures we can never know anything about. May 22, 2022 at 1:22

7 Answers 7

25

As Thorbjørn Ravn Andersen already put it nicely:

What would you need it for?

There is almost no practical (*1) need to obtain the PC address at runtime (*2) - it's a value to be obtained during assembly time, provided by Assembler and/or Linker. A simple

HERE:   LEA  AX,HERE   ;(*3)

will make sure the Assembler and/or Linker puts the actual instruction's address into a register (AX in this case).

Now, if you really want to do the trick, than best do it like it would work with any CPU: Jump one instruction ahead by using a subroutine call and then pop the return address.

       CALL  NEAR PTR NEXT ; Make sure it's not a far call *4
NEXT:  POP   AX

Except, there's a major caveat:

Above is trouble-free only in clean 16-bit code. Different addressing modes may require use of 32-bit registers and more.

Going for tricks like this is a sure way to introduce incompatibilities. It's the old story of programming what you want to do, not how to do it. Letting your tools, compiler/assembler/linker, do the 'dirty' work ensures it gets performed the best possible way.


Further Reading:

As said, there is no real use for IP being one of the accessible registers; many quite successful architectures (/360, 68k, 8080, 6500, etc) do not have a directly accessible PC. They have at most a PC relative addressing (68k).

The PC is never a 'normal' register but is tied to the basic mechanics of a processor. In fact, it brings a lot of advantages (*4) having it completely separated and not readable, as on 8086 and others. It offers the most simple way to separate operational housekeeping (like (pre-) fetching) from logical operation. The only use case for storing it is in the case of a subroutine branch. This can be handled keeping a shadow copy of the next address to be executed.

Architectures that allow the use of a PC in addressing may need to hold a second shadow copy with the instruction's address, complicating it. Architectures that include the PC in the register set, or use one of their GP registers as PC, need to take care about several constraints. One visible sign is that some RISC implementations do need to use PC relative addressing with a constant offset from the actual location. But there is more.

Long story short: Better not care for the PC at all - beside jumping that is.


*1 - The "almost" case is dynamically created code - but even then it would be more appropriate to improve the generator.

*2 - And no, the standard use of BALR Rx,0 by /370 modules to get a local reference for jumps and constants is an oddity due to there being neither an absolute nor a PC relative addressing, nor the ability to load immediate word (address) values.

*3 - And yes, a MOV could be used as well in nearly all (simple) cases. I still prefer the LEA as it allows even more weird address generations :))

*4 - It still may run into trouble depending on addressing modes and memory model.

*5 - This is only an issue in CPUs with a certain level of asynchronous operation, like having prefetch or speculative operation.

3
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Chenmunka
    May 24, 2022 at 6:22
  • Practically every modern operating system requires such an instruction to implement shared libraries and position-independent code, to the extent of CPU manufacturers heavily optimizing the call/pop instruction sequence, which tended to be very slow as it confuses the return stack cache. Also, one of the major improvements of amd64 over x86 was the addition of rip-relative addressing. Jul 18, 2022 at 21:10
  • 1
    @RememberMonica Position Independent code does not need to access IP, it only needs IP relative (or entry relative) addressing. Likewise do shared libraries only need it (if at all) during initialization, so again no need to have a dedicated instruction. Adding one would be ISA bloat.
    – Raffzahn
    Jul 18, 2022 at 23:54
16

OP specifically clarifies interest in a historical reason. Intel would have to give their exact reasoning but the following points are worth noting.

Intel's 8086 and 8088 were outgrowths of their earlier 8008 and 4004 microprocessors - these architectures all had an address space that required more bits than their 16, 8 or 4-bit data width.

On the other hand, minicomputers of the day, including their microcomputer outgrowths, tended to have an orthogonal approach to registers in which the Program Counter and Stack Pointer were numbered in with the general purpose registers and could be accessed or used in a variety of addressing modes (cf. PDP-11 vs LSI-11 and TI990 vs TMS9900 - both originally true 16-bit architectures addressing 2¹⁶ bytes).

The ability to access the Program Counter was very useful and was a common idiom in accessing parameters and local variables stored with the code, and in implementing Position Independent Code or in contexts relating to linking and/or shifting and/or overlaying code blocks (and there were also various useful use-cases involving self-modifying code and dynamically generated/updated code).

One of the big issues with the PDP-11 and TI990 16-bit architectures, as well as earlier 8-bit and 16-bit microprocessors generally, was the inability to index more than 2¹⁶ units of memory. The PDP-11 family introduced models with separate Instruction and Data address spaces, while a more general approach introduced segments allowed for separate code and stack segments and multiple data segments - with its segment registers the 8086 was able to address a 2²⁰ physical memory. Segmentation also introduced the ability to provide different Read/Write/Execute permissions for different segments.

This, and the increasing use of recursion (which requires a stack), made it inappropriate to store code and data/parameters/locals together in adjacent addresses and means that addresses can't be interpreted without their segment address. That put an end to much of the need for and utility of direct access to the the Instruction Pointer.

8
  • 1
    This is a great answer to the question, with loads of supporting detail. Welcome to RCSE! May 21, 2022 at 10:11
  • Somewhat misleading arguing. 68k managed to use up to 2^32 bytes without segmentation and also had pc-relative addressing, including the ability to load address from PC to GPR.
    – lvd
    May 22, 2022 at 8:17
  • @lvd The 68000 is a couple of generations (a decade and a half) later and one of the first 32-bit microchips (alhough with a 16-bit data bus much as the 8088 is a 16-bit machine like the 8086 but with an 8-bit data bus). So it didn't need segmentation to address 2^32 bytes. Of course, paging is another approach to mapping logical address space into a potentially larger physical address and is easier for compilers to manage (so segmentation is phased out in x64). May 22, 2022 at 11:30
  • 2
    68000 wad quite contemporary to 8086.
    – lvd
    May 22, 2022 at 12:55
  • @DavidMWPowers: In what sense is paging "easier for compilers to manage"? Having a logical address space that's large enough to allow compilers to ignore paging is easy, but if one needs a larger address space segmentation is easier to work with. The only systems I know of where compilers "manage paging" have very small pages (256 bytes or less), and only allow one page to be active at a time.
    – supercat
    May 24, 2022 at 17:05
15

So long as a stack exists, the IP address may easily be obtained via the byte sequence "E8 00 00 5B" [CALL $+3 : POP BX] because near calls use PC-relative addressing. On the other hand, the normal state of affairs for position-independent code on the 8086 is to be located at a fixed address within an arbitrary segment, which allows zero-effort relocation on any 16-byte boundary.

Because such methods are available (in particular, the PC-relative addressing that enables this within a code block), there is less need to provide a specific instruction to access the instruction pointer.

12
  • 1
    Potential catch-22 in some scenarios: with PIC, you may wish to have your stack set to a fixed offset from your code. Which requires knowing the IP to calculate the load address to set SS appropriately...
    – TLW
    May 21, 2022 at 20:36
  • Just a note: NEVER do in this way in a real code! The proper way is to all a procedure that contains mov (esp),reg:ret
    – lvd
    May 22, 2022 at 8:08
  • 2
    @lvd Never say Never. The procedureless way is quite proper, and the most efficient way of doing it in certain usecases - e.g. code in *ROM where every byte counted (like on a computer with 2K RAM, 2K ROM and 2K EPROM and no disk, as I had in the late 70s; mid 70s, my machines had <1K). May 22, 2022 at 12:00
  • 1
    This answer makes no attempt to address the question "why is there no direct way to access the instruction pointer?". Whilst it might be interesting to show an indirect way to do it, that is an answer to an entirely different question. May 24, 2022 at 9:49
  • 1
    @TobySpeight: On the Z80, the only call instructions use a fixed target address (either following the opcode or embedded within it), which means one needs to know of a fixed address where one can find or place code to inspect the stack, but on the 8086 one can place the stack-inspection code in the same blob as the call instruction without having to know where it is. The ability to do that means that direct instruction to access the PC has far less value than it would on e.g. the Z80.
    – supercat
    May 24, 2022 at 14:55
12

On 8086 the instruction pointer is not a general purpose register you can freely access for reading. On earlier 808x models this was also the case, even though program counter was directly used to fetch instructions without a prefetch queue, and it was settable via PCHL instruction. Because the CPUs supported natively stack, jumping, and subroutines, the programming model how to write ordinary programs just did not need reading of IP, so opcodes and their parameters could be used for other more useful things. And it is still possible to read the position where the CPU is currently executing indirectly if there is need. At a quick glance, many other CPUs from approximately same era (Z80, 6500, 6800) also don't have an opcode to read PC/IP, likely from the same reason.

The CPU does not directly use the instruction pointer for execution, as the executed instructions are fetched from prefetch queue, and the queue is filled from memory.

The instruction pointer (IP) does not reside on the Execution Unit (EU) side, but on the Bus Interface Unit (BIU) side, with the segment registers.

So just like there is no instruction to directly set/store IP, because a jump or call must clear prefetch queue to make sure instructions are fetched for execution from correct address, there is also no instruction to get/load IP, because it likely won't point to the currently executed instruction.

So, whenever the actual value of IP is needed, such as when CALLing a subroutine to push correct value of IP to stack, the value is adjusted as needed and then stored to memory. So there is some logic to keep track of how the IP should be modified when the value is needed.

But internally, that's how the CPU works according to the user manual, with the IP pointing to the address of memory to be next fetched into queue.

7
  • "[the IP] likely won't point to the currently executed instruction" - The IP associated with the currently executed instruction must exist somewhere, else how could a relative branch work? May 20, 2022 at 17:00
  • 4
    In order for the CPU to be able to correctly process a CALL or INT instruction, it must know the address of the following instruction, even if instructions after that have been fetched and will need to be discarded. By my understanding, the 8086 maintains a Program Counter within the bus interface, and an Instruction Pointer within the execution unit. CALL and INT instructions save the Instruction Pointer; control-flow instructions load both the PC and IP with the new address.
    – supercat
    May 20, 2022 at 17:01
  • Of course there needs to be some logic how it works. However, the 8086 user manual says the instruction pointer is in the BIU, and it also says the IP points to the next instruction to be fetched by the BIU (emphasis from manual). IP is adjusted to point to next instruction to be executed if IP needs to be saved on stack.
    – Justme
    May 20, 2022 at 17:27
  • Forgot to mention IP will be adjusted by jumps too. And the prefetch queue is the reason why self-modifying code must ensure that the modified code is not already fetched into queue before modifying the memory. And why "POP CS" did exist but was made undocumented.
    – Justme
    May 20, 2022 at 17:43
  • @Justme: That seems a bit weird, since it would require that the BIU either have a full 16-bit ALU to perform relative jumps, or would require that the BIU adjust the PC, transfer it to the main execution unit, have it compute the new address, and send the PC back to the BIU. Also, what I remember from back in the day is that the reason the IP was called the IP rather than the PC was to distinguish the fact that the PC would often point a few bytes ahead of the IP.
    – supercat
    May 21, 2022 at 17:35
7

There's a highly-upvoted comment, also requoted in this question's accepted answer, saying What would you need it for? For what it's worth, here is a real, practical, albeit reasonably obscure example.

Once upon a time I wrote a C interpreter. One of its goals was to allow interoperation between interpreted and previously compiled code. It contained its own dynamic linker, so that it could read in object and library files, and call functions in them. (This is the same sort of thing that dlopen does today.)

Besides interpreted code making calls to compiled functions, it was also possible for compiled code to call back to interpreted code. Without going into all the gory details, this meant that a pointer to the interpreter's data structure describing an interpreted function had to also be usable as an actual function pointer. This data structure therefore began (that is, at offset 0) with a little data block containing trampoline code which was contrived to fire up the interpreter on the just-called function. And the very first thing the trampoline code had to do was, naturally enough, fetch its own PC, because that value was actually the pointer to the data structure describing the function to be interpreted. (Needless to say this was long before execute protection on data pages, and stuff like that.)

The first processor I wrote this for was the PDP-11, where I found myself able to do something straightforward like mov pc,r0. Not long after, I ported it to the VAX, which trapped on me when I tried to access my own PC in such a crude and obvious way, but after some digging around I was able to achieve the desired effect via a mild circumlocution such as lea 0@pc or something like that. I was certainly aware that what I was doing was dicey and difficult: the PC is not a general-purpose register, for all the sorts of reasons described in the other answers here. So I wasn't surprised I needed a circumlocution, and I was pleased to find one that worked.

(Later on I managed to port the interpreter, with some new trampoline code, to an 80286 or 80386 under MS-DOS, but I don't remember which instructions I used or how clever I had to be. Much later I somehow got it working on the same x86-based Mac where I'm typing this today. The gory, handcrafted trampoline code is gone, replaced by a magic libffi closure.)

So, anyway, this is an example of why someone might legitimately need — or once have needed — to directly access the program counter.

12
  • 1
    Well, nice to know why it would be useful to have such an instruction, but it still does not answer the question why there is no x86 opcode for directly getting the current value of IP, and even without such an instruction, it is easy to indirectly fetch it. Regardless of the CPU architecture, you have to work with the registers and instructions you happen to have.
    – Justme
    May 21, 2022 at 7:56
  • 2
    [cont] If (as is often the case: no reflection on you or the original OP), it's an "X-Y" problem (an OP wants to do "something", and thinks knowing the PC will help), then people may be able to point the OP in better ways of solving the core problem. In your particular case, depending when you asked, you might have been told of LEA 0@PC, have been directed to libffi, or – indeed – your genuine need might have sparked the development of something like libffi in the first place!
    – TripeHound
    May 21, 2022 at 8:06
  • 2
    Second, I understand why you are proud of your clever hack even though I do not fully understand which original problem you solved (avoiding having to run the compiler?). I noted, however, that you found the required handcrafted (probably meaning raw assembly) trampoline code difficult to maintain and replaced it. May 21, 2022 at 10:51
  • 2
    Third, "fell into disuse" sounds like it wasn't used by many others than you. How come? May 21, 2022 at 10:54
  • 3
    Fourth, I agree with Raffzahn that this does not answer the original question at all (which may cause it to be closed and deleted) and therefore is probably better placed somewhere else. Perhaps on your personal blog with a lot more details? May 21, 2022 at 11:16
0

A readable PC may require extra hardware resources, perhaps a dedicated set of unidirectional wires from the PC to some write Dest reg data path. Fetching instructions and pushing the PC only requires wires from the PC to the memory access unit.

So why add wires to the chip area budget unless there’s profitable payback (performance, etc.)

-2

It does. call 1f; 1: is "push ip".

3
  • 2
    This was already mentioned in supercat’s answer. May 22, 2022 at 16:58
  • @wizzwizz4: Calling to a label immediately after the call instruction does not change the flow of execution but pushes the address immediately after the call instruction (the new value of ip) onto the stack (as the "return address"). May 22, 2022 at 17:19
  • @wizzwizz4: Nothing to do with linker. It's just how you write a call with zero displacement in asm mnemonics. May 22, 2022 at 17:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .