True -- but such portability can be overestimated. For instance,
the Lance Ethernet chip has two 16-bit registers (the "RAP" and
"RDP", or Register Address Port and Register Data Port) -- so we
(read "I") wrote a driver using the following data structure
(actually this predated C99 uint16_t; I used plain old unsigned
short):
struct lereg1 {
uint16_t ler1_rdp;
uint16_t ler1_rap;
};
Now, you might think a "volatile struct lereg1 *" variable would
suffice to access these on any machine on which the hardware can
be plugged in -- but you would be wrong.
On the MIPS as used in the DECstation, for instance, the data
structure above is wrong. It needs to read:
struct lereg1 {
uint16_t ler1_rdp;
uint16_t ler1_pad1;
uint16_t ler1_rap;
uint16_t ler1_pad2; /* optional */
};
The reason is that the CPU always does 32-bit transactions on the
bus, so in order to make sure that I/O to the RDP does not also
read or write the RAP (and vice versa), the hardware is physically
wired up with Lance address line A1 connected to bus address line
A2.
Now, one might dismiss this as a special case ... but then there
are architectures in which accesses to "memory" that does not
actually work like RAM must be preceded and/or followed by specal
I/O-barrier instructions. These instructions should (perhaps
even "must") *not* be used for accesses to shared-memory variables,
however, which would also be labeled "volatile", hence the compiler
should not automatically insert them. Instead, C code that reads
or writes the RAP and RDP needs explicit barrier instructions as
well.
In short, while abstracting the details is a good idea, it only
takes you partway. Using "volatile" and other such tricks is
necessary, but may not be sufficient. As an OS writer, I just code
what I know is needed now and try to leave enough room for later
"improvements".
I don't agree. Some feature can be only implemented in assembly. How can we
access the system register CR0 by C language ?
Indeed, some architectures make it impossible to do anything
"not memory-like" using "memory-like" accesses. For instance,
instead of reading the RAP and RDP with "volatile struct lereg1 *"
pointers, we might have to do something like:
io_port_write(LANCE_RAP, 1); /* point to register 1 */
result = io_port_read(LANCE_RDP); /* read register 1 */
/* replaces: reg->ler1_rap = 1; result = reg->ler1_rdp; */
On the Intel x86 in particular, one may need to use the "inw" and
"outw" instructions here.
Some (many?) C compilers offer some sort of "escape to assembler"
feature so that one can write inw() and outw() without having to
use actual subroutine calls (a waste of both code space and CPU
time, on the x86). These features are not only hardware-dependent,
they are compiler-dependent as well. GCC's "__asm__ volatile"
syntax does not work in Microsoft C compilers. (You need the
"volatile" as well as the __asm__ keyword to prevent GCC from moving
and combining the instruction when it has indescribable side
effects.)
What this all boils down to, though, is what I think Lew Pitcher
was getting at here:
which can be rephrased as: "If -- and only if -- the hardware is
*extremely* cooperative (and hardware never is), one could write
an entire OS without ever stepping outside the bounds of C syntax
and with only `slightly dubious' semantics, such as assuming that
particular hardware devices are at particular numeric addresses,
and that casting integer constants to the right pointer types allows
you to access them this way." The source code would not be portable,
and even changing the compiler could cause it to fail (if, e.g.,
the new compiler does not have the same semantics for
integer-to-"pointer-to-hardware-device" conversions).
Since the hardware *never* cooperates to this extent, and since
the constructs needed for working around the balky hardware are
themselves not portable, operating systems are never written in
nothing but Standard C. At best, the majority of the OS code
is Standard C, so that only a small minority of code needs to be
rewritten for each port (to new hardware and/or new compilers).
(Performance factors often get in the way here too -- more code
is machine- and compiler-dependent than strictly necessary, in
these OSes, because the result is a system that runs 30% faster.)