Is the aliasing rule symmetric?

  • Thread starter Johannes Schaub (litb)
  • Start date
J

Johannes Schaub (litb)

Posting my SO question to usenet:

Hello all. I had a discussion with someone on IRC and this question turned
up. We are allowed by the Standard to change an object of type `int` by a
`char` lvalue.

int a;
char *b = (char*) &a;
*b = 0;

Would we be allowed to do this in the opposite direction, if we know that
the alignment is fine?

The issue I'm seeing is that the aliasing rule does not cover the simple
case of the following, if one considers the aliasing rule as a non-symmetric
relation

int a;
a = 0;

The reason is, that each object contains a sequence of `sizeof(obj)`
`unsigned char` objects (called the "object representation"). If we change
the `int`, we will change some or all of those objects. However, the
aliasing rule only states we are allowed to change a `int` by an `char` or
`unsigned char`, but not the other way around. Another example

int a[1];
int *ra = a;
*ra = 0;

Only one direction is described by 3.10/15 ("An aggregate or union type that
includes..."), but this time we need the other way around ("A type that is
the element or non-static data member type of an aggregate...").

Is the other direction implied? The above spec quotes are from C++, but I
beleive same things apply to C.
 
S

SG

Hello all. I had a discussion with someone on IRC and this question turned
up. We are allowed by the Standard to change an object of type `int` by a
`char` lvalue.

    int a;
    char *b = (char*) &a;
    *b = 0;

Would we be allowed to do this in the opposite direction, if we know that
the alignment is fine?

I think the question boils down to another question: When does a POD
object start to exist? One could argue that once you have properly
aligned memory to hold an int object and the byte sequence represents
a valid state of an int object, there *is* an int object. Consider

int* p = (int*)malloc(sizeof int);
*p = 1729;

malloc returns properly aligned uninitialized memory. When does the
int object start to exist?

Under the assumption that an int objects exists at some address as
soon as the byte sequence starting at that address represents a valid
int object's state and the memory is properly aligned, the aliasing
rule is practically symmetric.

Cheers!
SG
 
B

Ben Bacarisse

Johannes Schaub (litb) said:
Posting my SO question to usenet:

I don't find this question all that clear, but perhaps my confusion will
help make it clearer!
Hello all. I had a discussion with someone on IRC and this question turned
up. We are allowed by the Standard to change an object of type `int` by a
`char` lvalue.

int a;
char *b = (char*) &a;
*b = 0;

OK so far.
Would we be allowed to do this in the opposite direction, if we know that
the alignment is fine?

What's the opposite direction? Are you asking if changing the int will
change the value of *b? If so, yes (provided the new int value's bits
do indeed affect the byte in question).

If you mean can we change an array of char (suitable aligned) by writing
though and int *, then the answer is no.
The issue I'm seeing is that the aliasing rule does not cover the simple
case of the following, if one considers the aliasing rule as a non-symmetric
relation

int a;
a = 0;

The wording in the C standard covers that case explicitly:

An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:

-- a type compatible with the effective type of the object,
-- (etc...)

'a' is an lvalue expression compatible with the effective type of the
object denoted by 'a'.

C++ does not use the concept of effective type and it does not used C's
notion of compatible types, so there are four clauses in 3.10 p15 that
capture the same semantics as this one clause in the C standard.
The reason is, that each object contains a sequence of `sizeof(obj)`
`unsigned char` objects (called the "object representation"). If we change
the `int`, we will change some or all of those objects. However, the
aliasing rule only states we are allowed to change a `int` by an `char` or
`unsigned char`, but not the other way around. Another example

int a[1];
int *ra = a;
*ra = 0;

Only one direction is described by 3.10/15 ("An aggregate or union type that
includes..."), but this time we need the other way around ("A type that is
the element or non-static data member type of an aggregate...").

I may be missing your point, but this case seems to me perfectly well
covered by the 6.5 p7 in C and 3.10 p15 in C++. The effective type of
*ra is int and that is compatible (actually the same as) the effective
type of the object being accessed. In C++ the access is through an
lvalue expression with the dynamic type of the object (both are int).
 
J

Johannes Schaub (litb)

Ben said:
What's the opposite direction? Are you asking if changing the int will
change the value of *b? If so, yes (provided the new int value's bits
do indeed affect the byte in question).

I mean to ask: If aliasing of an A object by an lvalue of type B is OK, is
aliasing of a B object by an lvalue of type A OK?
If you mean can we change an array of char (suitable aligned) by writing
though and int *, then the answer is no.


The wording in the C standard covers that case explicitly:

An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:

-- a type compatible with the effective type of the object,
-- (etc...)

'a' is an lvalue expression compatible with the effective type of the
object denoted by 'a'.

C doesn't seem to have this wording. But C++ says:

The object representation of an object of type T is the sequence of N
unsigned char objects taken up by the object of type T, where N equals
sizeof(T).

However I may well be wrong here, and this is only a conceptual description,
and not really a description of actual unsigned char objects covering the
same storage. In that case, the below array case and the struct case show
other situations making my point.
The reason is, that each object contains a sequence of `sizeof(obj)`
`unsigned char` objects (called the "object representation"). If we
change the `int`, we will change some or all of those objects. However,
the aliasing rule only states we are allowed to change a `int` by an
`char` or `unsigned char`, but not the other way around. Another example

int a[1];
int *ra = a;
*ra = 0;

Only one direction is described by 3.10/15 ("An aggregate or union type
that includes..."), but this time we need the other way around ("A type
that is the element or non-static data member type of an aggregate...").

I may be missing your point, but this case seems to me perfectly well
covered by the 6.5 p7 in C and 3.10 p15 in C++. The effective type of
*ra is int and that is compatible (actually the same as) the effective
type of the object being accessed. In C++ the access is through an
lvalue expression with the dynamic type of the object (both are int).

The above changes two objects. The first has type int, and the second has
type int[1]. The change of a int[1] by an lvalue of type int is not covered
anywhere at 3.10 p15 in C++. It's not covered by the C rules either, it
seems.

Same situation with structs:

struct A { int a; };
A a;
int *p = &a.a;
*p = 0;

This changes the stored value of an A object by an lvalue of type int. Where
is this covered?

Is my understanding of this wrong?
 
B

Ben Bacarisse

Johannes Schaub (litb) said:
I mean to ask: If aliasing of an A object by an lvalue of type B is OK, is
aliasing of a B object by an lvalue of type A OK?

No, not as far as I can see. Let's assume a system with sizeof(int) ==
1 and where int requires no more alignment than char. On such a system
this

int a;
char *cp = (void *)&a;
*cp = 42;

is fine by C's rules (and the intent is probably that C++ be the same in
this regard) but this

char a;
int *ip = (void *)&a;
*ip = 42;

is undefined. It is explicitly undefined by 6.5 p7 whereas the first is
explicitly permitted by that text.
C doesn't seem to have this wording.

What wording? The indented text is from section 6.5 paragraph 7 in
n1256.pdf (I don't have the "real thing").
But C++ says:

The object representation of an object of type T is the sequence of N
unsigned char objects taken up by the object of type T, where N equals
sizeof(T).

There is similar wording for C in 6.2.6.1 p4. It is not exactly the
same but I think it has pretty much the same effect. However, I am not
sure how this wording relates to your question.
However I may well be wrong here, and this is only a conceptual description,
and not really a description of actual unsigned char objects covering the
same storage. In that case, the below array case and the struct case show
other situations making my point.
The reason is, that each object contains a sequence of `sizeof(obj)`
`unsigned char` objects (called the "object representation"). If we
change the `int`, we will change some or all of those objects. However,
the aliasing rule only states we are allowed to change a `int` by an
`char` or `unsigned char`, but not the other way around. Another example

int a[1];
int *ra = a;
*ra = 0;

Only one direction is described by 3.10/15 ("An aggregate or union type
that includes..."), but this time we need the other way around ("A type
that is the element or non-static data member type of an aggregate...").

I may be missing your point, but this case seems to me perfectly well
covered by the 6.5 p7 in C and 3.10 p15 in C++. The effective type of
*ra is int and that is compatible (actually the same as) the effective
type of the object being accessed. In C++ the access is through an
lvalue expression with the dynamic type of the object (both are int).

The above changes two objects. The first has type int, and the second has
type int[1]. The change of a int[1] by an lvalue of type int is not covered
anywhere at 3.10 p15 in C++. It's not covered by the C rules either, it
seems.

I see your point but I don't think that's what is intended. The object
being accessed is an int, not the array that contains it. The fact that
the access changes a larger containing object does not seem to alter the
validity of the access in question. Note the wording (in C) is all
about the access, not about what that access changes.
Same situation with structs:

struct A { int a; };
A a;
int *p = &a.a;
*p = 0;

This changes the stored value of an A object by an lvalue of type int. Where
is this covered?

Is my understanding of this wrong?

I think it is covered in C and in C++ because what is being accessed is
'a.a' not 'a'. The fact that a changes as a result is not ruled out by
the wording that permits the access of the sub-object.
 
D

Dag-Erling Smørgrav

Ben Bacarisse said:
No, not as far as I can see.

The correct answer is "it depends". The OP used an example where B is
char and A is something larger, which is a special case that works in
one direction but not the other, but if e.g. A and B are struct types
with a common prefix, the answer is yes. A good real-world example is
the various flavors of struct sockaddr in the BSD socket API.

DES
 
M

Marcin Grzegorczyk

Johannes said:
Hello all. I had a discussion with someone on IRC and this question turned
up. We are allowed by the Standard to change an object of type `int` by a
`char` lvalue.

int a;
char *b = (char*)&a;
*b = 0;

Would we be allowed to do this in the opposite direction, if we know that
the alignment is fine?

Apparently not, as far as C is concerned. By 6.5p6 [all chapter and
verse references to N1256 unless indicated otherwise], "The effective
type of an object for an access to its stored value is the declared type
of the object, if any".

One consequence of the effective type rules is that the common technique
of providing a custom memory allocation scheme for freestanding
implementations, using a static array of unsigned char as the memory
pool, leads to undefined behaviour when you try to use memory allocated
on that pool as, say an array of ints -- even if you have a way to
specify the alignment (which C1X will provide). Indeed, the only
standard C way to get storage without declared type is malloc() and
friends, which of course need not be available in a freestanding
implementation.

[...]
Another example

int a[1];
int *ra = a;
*ra = 0;

Only one direction is described by 3.10/15 ("An aggregate or union type that
includes..."), but this time we need the other way around ("A type that is
the element or non-static data member type of an aggregate...").

Is the other direction implied? The above spec quotes are from C++, but I
beleive same things apply to C.

Yes, C99 has the same wording (6.5p7). Incidentally, the ISO C
Committee has already acknowledged that this exact clause is confusing,
and even tried - twice - to improve the effective type rules (see WG14
documents N1409 and N1520); in both cases, the decision was "more work
needed".
 
L

lawrence.jones

In comp.std.c "Johannes Schaub (litb) said:
The above changes two objects. The first has type int, and the second has
type int[1]. The change of a int[1] by an lvalue of type int is not covered
anywhere at 3.10 p15 in C++. It's not covered by the C rules either, it
seems.

Same situation with structs:

struct A { int a; };
A a;
int *p = &a.a;
*p = 0;

This changes the stored value of an A object by an lvalue of type int. Where
is this covered?

I can't speak for C++, but those cases are both covered in C by the next
to last rule in 6.5p7 ("an aggregate or union type that includes one of
the aforementioned types...").
 
B

Ben Bacarisse

Dag-Erling Smørgrav said:
The correct answer is "it depends". The OP used an example where B is
char and A is something larger, which is a special case that works in
one direction but not the other, but if e.g. A and B are struct types
with a common prefix, the answer is yes. A good real-world example is
the various flavors of struct sockaddr in the BSD socket API.

Maybe there is C/C++ difference here but in C accessing a struct A as
if it were a struct B is undefined[1]. The old BSD socket API is going
to work because the implementation can't use the permission that the
aliasing rule give it in any way that would defeat the code. However,
in code such as this:

struct s1 { int i; double d; };
struct s2 { int i; float f; };

int f(struct s1 *s1p, struct s2 *s2p)
{
int x = s1p->i;
s2p->i = 42;
return x * s1p->i;
}

an implementation is permitted to assume that s1p->i has not changed and
to re-use the value of x in the return.

In some sense you are correct to say that it depends because there are
examples where Johannes Schaub's rule is OK (for example when A is char
and B is unsigned char) but I took the question to be a general one: is
it always OK to reverse the types? That's why I said "no" and gave a
single counter-example.

[1] There is a special clause in C about structs that share an initial
segment when both are members of the same union, but that is not what
you are talking about.
 
J

Joshua Maurice

I mean to ask: If aliasing of an A object by an lvalue of type B is OK, is
aliasing of a B object by an lvalue of type A OK?

No. Don't think about it as an aliasing rule. Think about it as a rule
which restricts the types of lvalues with which you can legally access
objects.

You can always access an object through a char or unsigned char
lvalue. (Or maybe it's only for POD types - there's no consensus. I
would only use char and unsigned char to access POD objects.)

You can always access an object through a base class lvalue, but you
can never do the reverse: you can never take a complete object of type
T and access it through a derived type of type T.
 
J

Joshua Maurice

The above changes two objects. The first has type int, and the second has
type int[1]. The change of a int[1] by an lvalue of type int is not covered
anywhere at 3.10 p15 in C++. It's not covered by the C rules either, it
seems.
Same situation with structs:
    struct A { int a; };
    A a;
    int *p = &a.a;
    *p = 0;
This changes the stored value of an A object by an lvalue of type int. Where
is this covered?

I can't speak for C++, but those cases are both covered in C by the next
to last rule in 6.5p7 ("an aggregate or union type that includes one of
the aforementioned types...").

I can't speak to the exact standardeze offhand, but accessing an
object through an appropriately created lvalue referring to a sub-
object is perfectly sensible behavior. "*p" is an appropriately
obtained lvalue which refers to a sub-object of the complete object,
and it's perfectly fine to access the complete object through that
lvalue to sub-object.

By appropriately obtained, I mean that it actually "points to" the sub-
object. Ex:

struct Foo { int a; int b; };
int main()
{
Foo f;
int* x = reinterpret_cast<int*>(&f) + 1;
*x = 1;
return f.b;
}

The above is definitely not portable, and has undefined behavior on at
least some platforms. There's no guarantee that "*x" actually refers
to the sub-object "f.b". You need to obtain it through "appropriate"
means, such as:

struct Foo { int a; int b; };
int main()
{
Foo f;
int* x = & f.b;
*x = 1;
return f.b;
}
 
J

Johannes Schaub (litb)

In comp.std.c "Johannes Schaub (litb) said:
The above changes two objects. The first has type int, and the second has
type int[1]. The change of a int[1] by an lvalue of type int is not
covered anywhere at 3.10 p15 in C++. It's not covered by the C rules
either, it seems.

Same situation with structs:

struct A { int a; };
A a;
int *p = &a.a;
*p = 0;

This changes the stored value of an A object by an lvalue of type int.
Where is this covered?

I can't speak for C++, but those cases are both covered in C by the next
to last rule in 6.5p7 ("an aggregate or union type that includes one of
the aforementioned types...").

Well, that's the exact other way around. I'm using an int lvalue to access
the value of an object whose effective type is A. But that rule only grants
me to use an lvalue of type A to change the stored value of the member
object whose effective type is int.

Am I missing something?
 
J

James Kuyper

Dag-Erling Smørgrav said:
The correct answer is "it depends". The OP used an example where B is
char and A is something larger, which is a special case that works in
one direction but not the other, but if e.g. A and B are struct types
with a common prefix, the answer is yes. A good real-world example is
the various flavors of struct sockaddr in the BSD socket API.

Maybe there is C/C++ difference here but in C accessing a struct A as
if it were a struct B is undefined[1]. ....
[1] There is a special clause in C about structs that share an initial
segment when both are members of the same union, but that is not what
you are talking about.

I think that this is precisely what he was was referring to as a "common
prefix".
 
J

James Kuyper

In comp.std.c "Johannes Schaub (litb) said:
The above changes two objects. The first has type int, and the second has
type int[1]. The change of a int[1] by an lvalue of type int is not
covered anywhere at 3.10 p15 in C++. It's not covered by the C rules
either, it seems.

Same situation with structs:

struct A { int a; };
A a;
int *p =&a.a;
*p = 0;

This changes the stored value of an A object by an lvalue of type int.
Where is this covered?

I can't speak for C++, but those cases are both covered in C by the next
to last rule in 6.5p7 ("an aggregate or union type that includes one of
the aforementioned types...").

Well, that's the exact other way around. I'm using an int lvalue to access
the value of an object whose effective type is A. But that rule only grants
me to use an lvalue of type A to change the stored value of the member
object whose effective type is int.

Am I missing something?

No - the reverse is not allowed: trying to access an object of type int
using an lvalue of type struct A has undefined behavior. Consider that
struct A might be padded to an alignment larger than sizeof(int), and
consider that many of the operations permitted on an lvalue of struct
type can access bytes that are part of that struct, but not part of the
'a' member.

It's unlikely to be true in this case, but it would be more likely if
'a' were a smaller non-character type, such as a 16-bit short, on a
machine with a large word size, say 64-bits. A compiler for such a
machine might pad a struct 'A' to 64 bits. Then, when writing the value
of the a member of a struct 'A', it might write the entire 64 bits,
confident that the extra 48 bits would end up in memory reserved for the
struct. That will cause problems if your lvalue of type struct A
actually refers to memory containing a short that was an element in an
array of shorts. In that case, those other 48 bits would contain other
elements of that array.

You can prove the existence of a similar aliasing problem involving
arrays and their elements, by simply exchanging the roles of the array
type and the struct type in the above argument.
 
J

James Kuyper

On 01/21/2011 06:36 PM, Joshua Maurice wrote:
....
No. Don't think about it as an aliasing rule. Think about it as a rule
which restricts the types of lvalues with which you can legally access
objects.

What distinction is there between an aliasing rule and that description?
That description seems, to me, to be a fairly good definition of what
"aliasing rule" means, at least in the context of C or C++.
You can always access an object through a char or unsigned char
lvalue. (Or maybe it's only for POD types - there's no consensus. I
would only use char and unsigned char to access POD objects.)

I don't have a copy of the current C++ standard, nor of the latest draft
of the next standard - the closest that I have is n3035.pdf. In 3.10p15,
it makes the same exception for access through an lvalue of char and
unsigned char type that C does, and that exception is not tied to the
POD-ness of the dynamic type.
 
J

Johannes Schaub (litb)

James said:
The above changes two objects. The first has type int, and the second
has type int[1]. The change of a int[1] by an lvalue of type int is not
covered anywhere at 3.10 p15 in C++. It's not covered by the C rules
either, it seems.

Same situation with structs:

struct A { int a; };
A a;
int *p =&a.a;
*p = 0;

This changes the stored value of an A object by an lvalue of type int.
Where is this covered?

I can't speak for C++, but those cases are both covered in C by the next
to last rule in 6.5p7 ("an aggregate or union type that includes one of
the aforementioned types...").

Well, that's the exact other way around. I'm using an int lvalue to
access the value of an object whose effective type is A. But that rule
only grants me to use an lvalue of type A to change the stored value of
the member object whose effective type is int.

Am I missing something?

No - the reverse is not allowed: trying to access an object of type int
using an lvalue of type struct A has undefined behavior.

That's not true from an aliasing point of view. 3.10/15 explicitly allows
this:

an aggregate or union type that includes one of the aforementioned
types among its elements or non-static data members
Consider that
struct A might be padded to an alignment larger than sizeof(int), and
consider that many of the operations permitted on an lvalue of struct
type can access bytes that are part of that struct, but not part of the
'a' member.

We would then violate alignment requirements. This is a different thing from
aliasing requirement though.
 
S

Seebs

On 01/21/2011 06:36 PM, Joshua Maurice wrote:
...
What distinction is there between an aliasing rule and that description?
That description seems, to me, to be a fairly good definition of what
"aliasing rule" means, at least in the context of C or C++.

return foo(int *i1, int *i2) {
*i1 = 1;
*i2 = 2;
return *i1;
}

The reason this might return either 1 or 2 is aliasing, but has nothing
to do with the types with which you can legally access objects.

-s
 
J

Joshua Maurice

        return foo(int *i1, int *i2) {
                *i1 = 1;
                *i2 = 2;
                return *i1;
        }

The reason this might return either 1 or 2 is aliasing, but has nothing
to do with the types with which you can legally access objects.

-s

Apparently it was cross-posted, and I didn't notice, and someone
replied without the cross posting, and I definitely didn't notice.

Seebs is right that
int foo(int *i1, int *i2) {
*i1 = 1;
*i2 = 2;
return *i1;
}
If i1 and i2 alias, then it returns 2. If they don't alias, then it
returns 1. This function doesn't have a violation of the "strict
aliasing rules", which would be better called "effective type access
rules".

Let's consider this function though:
int foo(int* x, short* y)
{
*x = 1;
*y = 2;
return 1;
}
int bar(int* x, short* y)
{
*x = 1;
*y = 2;
return *x;
}
Let's consider functions foo and bar. Let's suppose that x and y alias
in both. For function foo, there is no undefined behavior even though
both alias (at least according to what appears to be the prominent
interpretation of these rules). For function bar, if they alias, then
we have undefined behavior. Both have aliasing of short* and int*, but
only one has undefined behavior. It has undefined behavior because
there is a read of a short object through an int lvalue in function
bar.

Obviously aliasing is a required component of this analysis, but the
undefined behavior results from using an lvlaue to access an object.
You can use a char lvalue to access an int object, but you cannot use
an int lvalue to access a char object (or char array).
 
B

Ben Bacarisse

James Kuyper said:
Dag-Erling Smørgrav said:
I mean to ask: If aliasing of an A object by an lvalue of type B is
OK, is aliasing of a B object by an lvalue of type A OK?
No, not as far as I can see.

The correct answer is "it depends". The OP used an example where B is
char and A is something larger, which is a special case that works in
one direction but not the other, but if e.g. A and B are struct types
with a common prefix, the answer is yes. A good real-world example is
the various flavors of struct sockaddr in the BSD socket API.

Maybe there is C/C++ difference here but in C accessing a struct A as
if it were a struct B is undefined[1]. ...
[1] There is a special clause in C about structs that share an initial
segment when both are members of the same union, but that is not what
you are talking about.

I think that this is precisely what he was was referring to as a
"common prefix".

I don't see how it can be. For one thing, the various flavors of
struct sockaddr are not in a union object (at least they were not used
in that way the last time I used the BSD API).

But more importantly, the special exception is not an exception to the
effective type access rules. It is always possible (from the point of
view of these rules) to access a member of a union that is not the one
last stored -- the bits are simply reinterpreted (possibly as a value of
a different type. That is as true for structures that don't share a
common prefix as it is for ones that do (and it's true for non-structure
types as well). What 6.5.2.3 p5 does is ensure that there won't be any
surprises about what data you actually get when there is a common prefix.

If the exception given in 6.5.2.3 p5 were not there, the access itself
to these common prefix elements would not suddenly become undefined --
the only difference would be that you would not be able to rely on
getting the data you expect.
 
J

James Kanze

On 01/21/2011 06:36 PM, Joshua Maurice wrote:
...
What distinction is there between an aliasing rule and that description?
That description seems, to me, to be a fairly good definition of what
"aliasing rule" means, at least in the context of C or C++.
I don't have a copy of the current C++ standard, nor of the latest draft
of the next standard - the closest that I have is n3035.pdf. In 3.10p15,
it makes the same exception for access through an lvalue of char and
unsigned char type that C does, and that exception is not tied to the
POD-ness of the dynamic type.

The C++ standard makes the exception allowing access through
a char or unsigned char type in the case where "a program
attempts access the stored value of an object". I'm not sure of
the exact intent, but accessing the stored value means a read
access. The standard doesn't seem to say anything about
modifying, except for the case where the access is to
a nonmodifiable lvalue. I would hope that the intent would be
more or less:

float f = 3.14159;
unsigned char* pc = reinterpret_cast<unsigned char*>(&f);
printf("%u\n", *pc); // Legal.
*pc = 0xFF; // Legal? Or UB?
std::cout << f << std::endl; // UB (maybe a trapping NaN)

There is (or should be) a clear exception, however, for memcpy
like operations: the following must be legal:

float f = 3.14159;
unsigned char buff[sizeof(float)];
memcpy(buff, &f, sizeof(float));
float other;
memcpy(&other, buff, sizeof(float));
printf("%.5f\n", other); // Must output 3.14159

This is in §3.9/3 in the C++ standard, and only for PODs in
C++. In this case, I'm fairly certain that the intent is for
C and C++ to be compatible in this regard, at least for PODs.

At any rate, the above more or less corresponds to what actually
happens on existing hardware. (To have similar problems with
int, you need some fairly exotic hardware; I think a Univac MPS
might fill the bill.)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,014
Latest member
BiancaFix3

Latest Threads

Top