Consider Intel x86 and AMD x86 Processors. As I understand, since they use the same Instruction Set, in theory an application compiled for Intel x86 processor would run without any modification on AMD x86 processor, even though these processors have different microarchitectures. Is my understanding correct? Or is there any exception to this rule?
Most binaries compiled for x86 will work on x86 because all x86 processors have the same binary instruction format.
Instruction set extensions are quite rare and happen after several consultations between the different vendors. For example, one of the most famous extension is the AES encryption extension to the x86 instruction set called AES-NI and developed by Intel. Both AMD and Intel added the extension to their processors in 2010.
The AES-NI instruction set extensions are used to optimize encryption and decryption algorithms on select Intel and AMD processors. Intel announced AES-NI in 2008 and released supported CPUs late 2010 with the Westmere architecture. AMD announced and shipped AES-NI support in 2010, starting with Bulldozer.
It isn't really true that extensions make x86 CPUs non-compatible because most extensions will be adopted by major vendors in the same year or so.
Code written to take advantage of extensions usually run only under compilers written by the vendor. For example, to take advantage of Intel Extensions you need to use the Intel Compiler, which won't work for AMD processors.
This is not really true. There is no "Intel compiler". Instead, you use an existing compiler with extensions in the compiler. It is up to the compiler to support an extension or not. Most of the time when developing software, you know the CPU you target and you assume that most clients won't be using a very old CPU. You thus simply assume that the targeted CPU supports a certain extension. For example, as to AES, most CPUs support AES-NI. You can safely assume that a user of your code has this extension on their processor and can thus use the extension.
In g++, you have the
-maes flag which allows to enable the AES-NI extension. You will thus be able to use these instructions either in inline assembly or let the compiler do it using certain optimization levels. This probably requires looking at the assembly output and doing some testing and reading g++ documentation. I'm not this much aware so I can't tell how that works exactly but you can have a look at an actual implementation here: https://stackoverflow.com/questions/32297088/how-to-implement-aes128-encryption-decryption-using-aes-ni-instructions-and-gcc.
The micro-architecture is how the ISA is implemented in the processor. It varies greatly between vendors. Both AMD and Intel have the same registers though (which could be considered a part of the micro-architecture). There exists several Model Specific Registers (MSR) and these are called differently from Intel or AMD. For example, there exists an MSR to do a system call in the kernel (https://stackoverflow.com/questions/66853510/who-sets-the-rip-register-when-you-call-the-clone-syscall/66863181#66863181). In Intel documentation it will be called IA32-LSTAR while for AMD it will be called LSTAR or STAR64. Both registers have the same number tough. Again, you have a good compatibility.
In your answer at first, you assumed that instructions to "talk to hardware" were different (for device drivers). This is also not commonly true. Today, most devices have very strong conventions that they must follow in order to be compatible with the computers they will be plugged in. For example, you have PCI for most peripherals, SATA for hard-drives, DDR3 for RAM, USB for other peripherals, ACPI for hardware detection and power management etc.
In most computers today, the only difference between using an AMD processor or an Intel processor is in the detection of the different hardware and the base addresses you are going to use to "talk" to this hardware. This could be considered a difference. For example, the base address of the PCI configuration space could be different between an AMD or an Intel processor. I'm not a hardware engineer but this is not necessarily due to the processor but mostly due to the motherboard or other components. Again, there is very good compatibility. The only difference it makes is that code written to detect PCI devices should take in consideration that the base address could change between computer and only use "relative" code as opposed to "absolute" code (use relative addresses instead of absolute and not assume that the address will be the same everywhere).
As a trivial problem, you can write a program that will try to print on which processor it is running, how many cores, how much memory etc. Such a program should obviously produce different results on an x86 and an AMD CPU.
On the other hand, it is quite possible to write code that runs correctly on a variety of very similar processors. AMD and x86 being "very similar" and not identical.
Many processors are based on some basic architecture, plus some extensions. Many developers check what extensions are available, and use them in order to get faster code by using those extensions. Now the set of bugs in the code that runs when the extension is available, and when it's not available, can be different, so behaviour could change.