473,805 Members | 2,164 Online
Bytes | Software Development & Data Engineering Community
+ Post

Home Posts Topics Members FAQ

Making Fatal Hidden Assumptions

We often find hidden, and totally unnecessary, assumptions being
made in code. The following leans heavily on one particular
example, which happens to be in C. However similar things can (and
do) occur in any language.

These assumptions are generally made because of familiarity with
the language. As a non-code example, consider the idea that the
faulty code is written by blackguards bent on foulling the
language. The term blackguards is not in favor these days, and for
good reason. However, the older you are, the more likely you are
to have used it since childhood, and to use it again, barring
specific thought on the subject. The same type of thing applies to
writing code.

I hope, with this little monograph, to encourage people to examine
some hidden assumptions they are making in their code. As ever, in
dealing with C, the reference standard is the ISO C standard.
Versions can be found in text and pdf format, by searching for N869
and N1124. [1] The latter does not have a text version, but is
more up-to-date.

We will always have innocent appearing code with these kinds of
assumptions built-in. However it would be wise to annotate such
code to make the assumptions explicit, which can avoid a great deal
of agony when the code is reused under other systems.

In the following example, the code is as downloaded from the
referenced URL, and the comments are entirely mine, including the
'every 5' linenumber references.

/* Making fatal hidden assumptions */
/* Paul Hsiehs version of strlen.
http://www.azillionmonkeys.com/qed/asmexample.html

Some sneaky hidden assumptions here:
1. p = s - 1 is valid. Not guaranteed. Careless coding.
2. cast (int) p is meaningful. Not guaranteed.
3. Use of 2's complement arithmetic.
4. ints have no trap representations or hidden bits.
5. 4 == sizeof(int) && 8 == CHAR_BIT.
6. size_t is actually int.
7. sizeof(int) is a power of 2.
8. int alignment depends on a zeroed bit field.

Since strlen is normally supplied by the system, the system
designer can guarantee all but item 1. Otherwise this is
not portable. Item 1 can probably be beaten by suitable
code reorganization to avoid the initial p = s - 1. This
is a serious bug which, for example, can cause segfaults
on many systems. It is most likely to foul when (int)s
has the value 0, and is meaningful.

He fails to make the valid assumption: 1 == sizeof(char).
*/

#define hasNulByte(x) ((x - 0x01010101) & ~x & 0x80808080)
#define SW (sizeof (int) / sizeof (char))

int xstrlen (const char *s) {
const char *p; /* 5 */
int d;

p = s - 1;
do {
p++; /* 10 */
if ((((int) p) & (SW - 1)) == 0) {
do {
d = *((int *) p);
p += SW;
} while (!hasNulByte (d)); /* 15 */
p -= SW;
}
} while (*p != 0);
return p - s;
} /* 20 */

Let us start with line 1! The constants appear to require that
sizeof(int) be 4, and that CHAR_BIT be precisely 8. I haven't
really looked too closely, and it is possible that the ~x term
allows for larger sizeof(int), but nothing allows for larger
CHAR_BIT. A further hidden assumption is that there are no trap
values in the representation of an int. Its functioning is
doubtful when sizeof(int) is less that 4. At the least it will
force promotion to long, which will seriously affect the speed.

This is an ingenious and speedy way of detecting a zero byte within
an int, provided the preconditions are met. There is nothing wrong
with it, PROVIDED we know when it is valid.

In line 2 we have the confusing use of sizeof(char), which is 1 by
definition. This just serves to obscure the fact that SW is
actually sizeof(int) later. No hidden assumptions have been made
here, but the usage helps to conceal later assumptions.

Line 4. Since this is intended to replace the systems strlen()
function, it would seem advantageous to use the appropriate
signature for the function. In particular strlen returns a size_t,
not an int. size_t is always unsigned.

In line 8 we come to a biggie. The standard specifically does not
guarantee the action of a pointer below an object. The only real
purpose of this statement is to compensate for the initial
increment in line 10. This can be avoided by rearrangement of the
code, which will then let the routine function where the
assumptions are valid. This is the only real error in the code
that I see.

In line 11 we have several hidden assumptions. The first is that
the cast of a pointer to an int is valid. This is never
guaranteed. A pointer can be much larger than an int, and may have
all sorts of non-integer like information embedded, such as segment
id. If sizeof(int) is less than 4 the validity of this is even
less likely.

Then we come to the purpose of the statement, which is to discover
if the pointer is suitably aligned for an int. It does this by
bit-anding with SW-1, which is the concealed sizeof(int)-1. This
won't be very useful if sizeof(int) is, say, 3 or any other
non-poweroftwo. In addition, it assumes that an aligned pointer
will have those bits zero. While this last is very likely in
todays systems, it is still an assumption. The system designer is
entitled to assume this, but user code is not.

Line 13 again uses the unwarranted cast of a pointer to an int.
This enables the use of the already suspicious macro hasNulByte in
line 15.

If all these assumptions are correct, line 19 finally calculates a
pointer difference (which is valid, and of type size_t or ssize_t,
but will always fit into a size_t). It then does a concealed cast
of this into an int, which could cause undefined or implementation
defined behaviour if the value exceeds what will fit into an int.
This one is also unnecessary, since it is trivial to define the
return type as size_t and guarantee success.

I haven't even mentioned the assumption of 2's complement
arithmetic, which I believe to be embedded in the hasNulByte
macro. I haven't bothered to think this out.

Would you believe that so many hidden assumptions can be embedded
in such innocent looking code? The sneaky thing is that the code
appears trivially correct at first glance. This is the stuff that
Heisenbugs are made of. Yet use of such code is fairly safe if we
are aware of those hidden assumptions.

I have cross-posted this without setting follow-ups, because I
believe that discussion will be valid in all the newsgroups posted.

[1] The draft C standards can be found at:
<http://www.open-std.org/jtc1/sc22/wg14/www/docs/>

--
"If you want to post a followup via groups.google.c om, don't use
the broken "Reply" link at the bottom of the article. Click on
"show options" at the top of the article, then click on the
"Reply" at the bottom of the article headers." - Keith Thompson
More details at: <http://cfaj.freeshell. org/google/>
Also see <http://www.safalra.com/special/googlegroupsrep ly/>

Mar 6 '06
351 13172
In article <dv**********@c anopus.cc.umani toba.ca> ro******@ibd.nr c-cnrc.gc.ca (Walter Roberson) writes:
In article <Iw********@cwi .nl>, Dik T. Winter <Di********@cwi .nl> wrote:
In article <11************ **********@z34g 2000cwc.googleg roups.com> "Ed Prochak" <ed*******@gmai l.com> writes:

I would agree that if an assembler must be a one-to-one mapping from
source line to opcode, then C doesn't fit. I just don't agree with that
definition of assembler.

On the other hand for every machine instruction there should be an
construct in the assembler to get that instruction. With that in
mind C doesn't fit either.


Over the years, there have been notable cases of "hidden" machine
instructions -- undocumented instructions, quite possibly with no
assembler construct (at least not in any publically available
assembler.)


Indeed. But even when we look at the published instructions C falls
short of providing a construct for every one. Where is the C construct
to do a multply step available in quite a few early RISC machines?
Note also that in assembler you can access the special bits indicating
overflow and whatever (if they are available on the machine). How to
do that in C?
--
dik t. winter, cwi, kruislaan 413, 1098 sj amsterdam, nederland, +31205924131
home: bovenover 215, 1025 jn amsterdam, nederland; http://www.cwi.nl/~dik/
Mar 14 '06 #161
In article <11************ *********@j33g2 000cwa.googlegr oups.com> "Ed Prochak" <ed*******@gmai l.com> writes:
....
C is an assembler because

-- It doesn't impose strict data type checking, especially between
integers and pointers.
(While there has been some discussion about cases where conversions
back and forth between them can fail, for most machines it works. Good
thing too or some OS's would be written in some other language.)
It does impose restrictions. You have to put in a cast.
-- datatype sizes are dependent on the underlying hardware. While a lot
of modern hardware has formed around the common 8bit char, and
multiples of 16 for int types (and recent C standards have started to
impose these standards), C still supports machines that used 9bit char
and 18bit and 36bit integers. This was the most frustrating thing for
me when I first learned C. It forces precisely some of the hidden
assumptions of this topic.
The same is true for Pascal.
-- C allows for easy "compilatio n" in that you could do it in one pass
of the source code (well two counting the preprocessor run). The
original C compiler was written in C so that bootstrapping onto a new
machine required only a simple easily written initial compiler to
compile the real compiler.
What is valid for C is also valid for Pascal. But not all is valid.
Code generation, for instance, is *not* part of the compiler. Without
a back-end that translates the code generated by the compiler to actual
machine instructions, you are still nowhere. So your simple easily written
initial compiler was not an easily written initial compiler. You have
to consider what you use as assembler as backend. In contrast the very
first Pascal compiler was really single-pass and generated machine code
on-the-fly, without backend. Everything ready to run. (BTW, on that
machine the linking stage was almost never pre-performed, it took only
a few milli-seconds.)
-- original versions of the C compiler did not have passes like
data-flow optimizers. So optimization was left to the programmer. Hence
things like x++ and register storage became part of the language.
Perhaps they are not needed now, but dropping these features from the
language will nearly make it a differrent language. I do not know of
any other HLL that has register, but about every assembler allows
access to the registers under programmer control.


In C "register" is only a suggestion, it is not necessary to follow it.
On the other hand, the very first Pascal compiler already did optimisation,
but not as a separte pass, but as part of the single pass it had. You
could tweek quite a bit with it when you had access to the source of the
compiler.
--
dik t. winter, cwi, kruislaan 413, 1098 sj amsterdam, nederland, +31205924131
home: bovenover 215, 1025 jn amsterdam, nederland; http://www.cwi.nl/~dik/
Mar 14 '06 #162
In article <pa************ *************** *@areilly.bpc-users.org> Andrew Reilly <an************ *@areilly.bpc-users.org> writes:
On Mon, 13 Mar 2006 15:31:35 +0000, Dik T. Winter wrote:

....
On the other hand for every machine instruction there should be an
construct in the assembler to get that instruction. With that in
mind C doesn't fit either.


Well, since we're talking about a "universal assembler" (a reasonably
commonly used term), that's obviously something different from the usual
machine-specific assembler, which does indeed usually have that property.
(Although I've met assemblers where the only way to get certain
instructions was to insert the data for the op-code in-line. Instruction
coverage sometimes lags behind features added to actual implementations .)


I have met one such, but not for the reason you think. In this case the
assembler knew the instruction, and translated it, but completely wrong.
Apparently an instruction never used, but nevertheless published. And I
needed it.

But what is (in your opinion) a universal assembler? What properties should
it have to contrast it with a HLL?
--
dik t. winter, cwi, kruislaan 413, 1098 sj amsterdam, nederland, +31205924131
home: bovenover 215, 1025 jn amsterdam, nederland; http://www.cwi.nl/~dik/
Mar 14 '06 #163
On Tue, 14 Mar 2006 03:13:08 +0000, Dik T. Winter wrote:
In article <pa************ *************** *@areilly.bpc-users.org> Andrew Reilly <an************ *@areilly.bpc-users.org> writes:
> On Mon, 13 Mar 2006 15:31:35 +0000, Dik T. Winter wrote:

...
> > On the other hand for every machine instruction there should be an
> > construct in the assembler to get that instruction. With that in
> > mind C doesn't fit either.

>
> Well, since we're talking about a "universal assembler" (a reasonably
> commonly used term), that's obviously something different from the usual
> machine-specific assembler, which does indeed usually have that property.
> (Although I've met assemblers where the only way to get certain
> instructions was to insert the data for the op-code in-line. Instruction
> coverage sometimes lags behind features added to actual implementations .)


I have met one such, but not for the reason you think. In this case the
assembler knew the instruction, and translated it, but completely wrong.
Apparently an instruction never used, but nevertheless published. And I
needed it.

But what is (in your opinion) a universal assembler? What properties should
it have to contrast it with a HLL?


I posted a page-long description of what I concieve a universal assembler
to be in a previous message in the thread. Perhaps it didn't get to your
news server? Google has it here:
http://groups.google.com/group/comp....8457481?hl=en&

The main properties that it would have, compared to a C (some other HLLs
do have some of these properties) are:

a) Rigidly defined functionality, without "optimizati on", except for
instruction scheduling, in support of VLIW or (some) superscaler cores.
(Different ways of expressing a particular algorithm, which perform more
or less efficiently on different architectures should be coded as such,
and selected at compile/configuration time, or built using
meta-programming techniques.) This is opposed to the HLL view which is
something like: express the algorithm in a sufficiently abstract way and
the compiler will figure out an efficient way to code it, perhaps. Yes,
compilers are really quite good at that, now, but that's not really the
point. This aspect is a bit like my suggestion in the linked post as
being something a bit like the Java spec, but without objects. Tao's
"Intent" VM is perhaps even closer. Not stack based. I would probably
still be happy if limited common-subexpression-elimination (factoring) was
allowed, to paper-over the array index vs pointer/cursor coding style vs
architecture differences.

b) Very little or no "language" level support for control structures or
calling conventions, but made-up-for with powerful compile-time
meta-programming facilities, and a standard "macro" library that provides
most of the expected facilities found in languages like C or Pascal. Much
of what are now thought of as compiler implementation features would wind
up in macro libraries. The advantage of this would be that code could be
written to *rely* on specific transformation performance and existence,
instead of just saying "hope that your compiler is clever enough to
recognize this idiom", in the documentation. It would also make possible
the sorts of small code factorizations that happen all the time in
assembly language, but which single-value-return, unnested function call
conventions in C make close to impossible. Or different coding styles,
like threaded interpreters, reasonable without language extensions.

I imagine something like LLVM (http://llvm.cs.uiuc.edu/), but with a
powerful symbolic compile-time macro language on top (eg scheme...), an
algepraic (infix) operator syntax, and an expression parser.

In the mean time, "C", not as defined by the standard, but as implemented
in the half dozen or so compilers that I regularly use, is not so far from
what I want, to make me put in the effort to build my universal assembler
myself.

Cheers,

--
Andrew

Mar 14 '06 #164
"Dik T. Winter" <Di********@cwi .nl> writes:
In article <11************ *********@j33g2 000cwa.googlegr oups.com>
"Ed Prochak" <ed*******@gmai l.com> writes:

[...]
> -- C allows for easy "compilatio n" in that you could do it in one pass
> of the source code (well two counting the preprocessor run). The
> original C compiler was written in C so that bootstrapping onto a new
> machine required only a simple easily written initial compiler to
> compile the real compiler.


What is valid for C is also valid for Pascal. But not all is valid.
Code generation, for instance, is *not* part of the compiler. Without
a back-end that translates the code generated by the compiler to actual
machine instructions, you are still nowhere. So your simple easily written
initial compiler was not an easily written initial compiler. You have
to consider what you use as assembler as backend. In contrast the very
first Pascal compiler was really single-pass and generated machine code
on-the-fly, without backend. Everything ready to run. (BTW, on that
machine the linking stage was almost never pre-performed, it took only
a few milli-seconds.)


This is a matter of terminology, but I'd say that code generation
certainly is part of the compiler. If the compiler generates machine
code directly that's certainly true. If it generates assembly
language and invokes a separate assembler, I'd still say that the
assembler is logically part of the compiler; both the front-end and
the assembler are part of translation phase 7 as described in the C
standard.

--
Keith Thompson (The_Other_Keit h) ks***@mib.org <http://www.ghoti.net/~kst>
San Diego Supercomputer Center <*> <http://users.sdsc.edu/~kst>
We must do something. This is something. Therefore, we must do this.
Mar 14 '06 #165
mw*****@newsguy .com (Michael Wojcik) wrote:
"Ed Prochak" <ed*******@gmai l.com> writes:
Keith Thompson wrote:
Most assembly languages are, of
course, machine-specific, since they directly specify the actual
instructions.
x++; in most machines can map to a single instruction


Sure, if "most machines" excludes load/store architectures, and
machines which cannot operate directly on an object of the size of
whatever x happens to be, and all the cases where "x" is a pointer to
an object of a size other than the machine's addressing granularity...

The presence in C of syntactic sugar for certain simple operations
like "x++" doesn't support the claim that C is somehow akin to
assembler in any case. One distinguishing feature of assembler is
a *lack* of syntactic sugar.


One can make a case that in at least one aspect, C is _less_ like
assembler than many other high-level languages: for. Most languages only
support for loops looking like FOR n=1 TO 13 STEP 2: NEXT or for n:=10
downto 1 do. Such loops are often easily caught in one, or a few, simple
machine instructions. Now try the same with for (node=root; node;
node=node->next) or for (i=1, j=10; x[i] && y[j]; i++, j--).

Richard
Mar 14 '06 #166
Richard wrote:
) One can make a case that in at least one aspect, C is _less_ like
) assembler than many other high-level languages: for. Most languages only
) support for loops looking like FOR n=1 TO 13 STEP 2: NEXT or for n:=10
) downto 1 do. Such loops are often easily caught in one, or a few, simple
) machine instructions. Now try the same with for (node=root; node;
) node=node->next) or for (i=1, j=10; x[i] && y[j]; i++, j--).

I disagree.

for (a; b; c) is trivially compiled to:

<compile a>
label-loop:
<compile b>
branch-if-false label-end
<compile c>
branch label-loop
label-end:

Which is a total of two branches and two branch labels.
So, two asm instructions comprise the whole for loop.
SaSW, Willem
--
Disclaimer: I am in no way responsible for any of the statements
made in the above text. For all I know I might be
drugged or something..
No I'm not paranoid. You all think I'm paranoid, don't you !
#EOT
Mar 14 '06 #167
Willem <wi****@stack.n l> wrote:
Richard wrote:
) One can make a case that in at least one aspect, C is _less_ like
) assembler than many other high-level languages: for. Most languages only
) support for loops looking like FOR n=1 TO 13 STEP 2: NEXT or for n:=10
) downto 1 do. Such loops are often easily caught in one, or a few, simple
) machine instructions. Now try the same with for (node=root; node;
) node=node->next) or for (i=1, j=10; x[i] && y[j]; i++, j--).

I disagree.

for (a; b; c) is trivially compiled to:

<compile a>
label-loop:
<compile b>
branch-if-false label-end
<compile c>
branch label-loop
label-end:

Which is a total of two branches and two branch labels.
So, two asm instructions comprise the whole for loop.


_If_ the computation of a, b and c are trivial. The point is that in C,
they can be as complex as you like. For the whole loop, you _could_ need

<200 machine instructions to initialise a>
label-loop:
<600 machine instructions to compute b>
branch-if-false label-end
[whatever is needed for the loop body]
<400 machine instructions to update c>
branch label-loop
label-end:

That's 1202 machine instructions for the loop itself. For the more
straightforward , count-only, Basic-and-Pascal kind, all you'll ever need
on many computers is:

store initial-value loop-register
label-loop:
[whatever is needed for the loop body]
decrease-and-jump-if-not-zero label-loop

That's 2.

Richard
Mar 14 '06 #168
)> for (a; b; c) is trivially compiled to:
)>
)> <compile a>
)> label-loop:
)> <compile b>
)> branch-if-false label-end
)> <compile c>
)> branch label-loop
)> label-end:
)>
)> Which is a total of two branches and two branch labels.
)> So, two asm instructions comprise the whole for loop.

Richard wrote:

) _If_ the computation of a, b and c are trivial. The point is that in C,
) they can be as complex as you like. For the whole loop, you _could_ need
)
) <200 machine instructions to initialise a>
) label-loop:
) <600 machine instructions to compute b>
) branch-if-false label-end
) [whatever is needed for the loop body]
) <400 machine instructions to update c>
) branch label-loop
) label-end:
)
) That's 1202 machine instructions for the loop itself. For the more

Those 1200 asm instructions are compiled from explicit code, written
out in full in the source code. The only thing the 'for' does,
is add two jumps in strategic places between those pieces of code.
SaSW, Willem
--
Disclaimer: I am in no way responsible for any of the statements
made in the above text. For all I know I might be
drugged or something..
No I'm not paranoid. You all think I'm paranoid, don't you !
#EOT
Mar 14 '06 #169

Keith Thompson wrote:
"Ed Prochak" <ed*******@gmai l.com> writes:
[...]
C is an assembler because
I should have phrased this: C is LIKE an assembler.

-- It doesn't impose strict data type checking, especially between
integers and pointers.
(While there has been some discussion about cases where conversions
back and forth between them can fail, for most machines it works. Good
thing too or some OS's would be written in some other language.)
Incorrect. Attempting to assign an integer value to a pointer object,
or vice versa, is a constraint violation, requiring a diagnostic.


a Warning.
Integer and pointers can be converted back and forth only using a cast
(an explicit conversion operator). The result of such a conversion is
implementation-defined.

Even if this were correct, it certainly wouldn't make C an assembler.
To some degree you are right. It's actually pointer manipulation that
makes it closer to assembler.
-- datatype sizes are dependent on the underlying hardware. While a lot
of modern hardware has formed around the common 8bit char, and
multiples of 16 for int types (and recent C standards have started to
impose these standards), C still supports machines that used 9bit char
and 18bit and 36bit integers. This was the most frustrating thing for
me when I first learned C. It forces precisely some of the hidden
assumptions of this topic.
I don't know what "recent C standards" you're referring to. C
requires CHAR_BIT to be at least 8; it can be larger. short and int
must be at least 16 bits, and long must be at least 32 bits. A
conforming implementation, even with the most current standard, could
have 9-bit char, 18-bit short, 36-bit int, and 72-bit long.


How about a bitsliced machine that uses only 6bit integers?

But this is a common feature of many high-level languages. Ada, for
example has an implementation-defined set of integer types, similar to
what C provides; I've never heard anyone claim that Ada is an
assembler.
Forgive my memory,but is it PL/1 or ADA that lets the programmer define
what integer type he wants. Syntax was something like
INTEGER*12 X
defined X as a 12 bit integer. (Note that such syntax is portable in
that on two different processors, you still know that the range of X is
+2048 to -2047
The point is a 16bit integer in ADA is always a 16bit integer and
writing
x=32768 +10
will always overflow in ADA, but it is dependent on the compiler and
processor in C. It can overflow, or it can succeed.

But my point on this was, you need to know your target processor in C
more than in a language like ADA. This puts a burden on the C
programmer closer to an assembler programmer on the same machine than
to a ADA programmer.
-- C allows for easy "compilatio n" in that you could do it in one pass
of the source code (well two counting the preprocessor run). The
original C compiler was written in C so that bootstrapping onto a new
machine required only a simple easily written initial compiler to
compile the real compiler.
You're talking about an implementation, not the language.


a big characteristic of assembler is that it is a simple language.
C is also a very simple language. Other HLLs are simple too, but the
simplicity combined with other characteristics suggest to me an
assembler feel to the language.
-- original versions of the C compiler did not have passes like
data-flow optimizers. So optimization was left to the programmer. Hence
things like x++ and register storage became part of the language.
Perhaps they are not needed now, but dropping these features from the
language will nearly make it a differrent language. I do not know of
any other HLL that has register, but about every assembler allows
access to the registers under programmer control.
Again, you're talking about an implementation, not the language.


No I was talking about the original motivation for the design of the
language. It was designed to exploit the register increment on DEC
processors. in the right context, (e.g. y=x++;) the increment doesn't
even become a separate instruction, as I mentioned in another post.

C doesn't allow access to specific registers (at least not portably).
But other HLL's don't even have register storage.
Here's what the C standard says about the "register" specifier:

A declaration of an identifier for an object with storage-class
specifier register suggests that access to the object be as fast
as possible. The extent to which such suggestions are effective is
implementation-defined.
I know that it is just a suggestion. The point is Why was it included
in the language at all? Initially it gave the programmer more control.
And there are a few restrictions; for example, you can't take the
address of a register-qualified object.
Which makes sense to an assembler programmer, but not to a typical HLL
programmer.
So IMHO, C is a nice generic assembler. It fits nicely in the narrow
world between hardware and applications. The fact that it is a decent
application development language is a bonus. I like C, I use it often.
Just realize it is a HLL with an assembler side too.


You've given a few examples that purport to demonstrate that C is an
assembler.

Try giving a definition of the word "assembler" . If the definition
applies to C (the language, not any particular implementation) , I'd
say it's likely to be a poor definition, but I'm willing to be
surprised.


lets put it this way. there is a gradient scale, from pure digits of
machine language (e.g., programming obcodes in binary is closer to the
hardware than using octal or hex)
at the lowest end and moving up past assebmler to higher and higher
levels of abstraction away from the hardware. On that scale, I put C
much closer to assembler than any other HLL I know. here's some samples
PERL, BASH, SQL
C++, JAVA
PASCAL, FORTRAN, COBOL
C
assembler
HEX opcodes
binary opcodes
digital voltages in the real hardware.

Boy. you'd think I was insulting C based on the length of this thread.
8^)

But maybe this made my position clearer.
Ed.

Mar 14 '06 #170

This thread has been closed and replies have been disabled. Please start a new discussion.

By using Bytes.com and it's services, you agree to our Privacy Policy and Terms of Use.

To disable or enable advertisements and analytics tracking please visit the manage ads & tracking page.