Using Type Modifier (signed) Comparisons - c

This prints "signed comparison" https://onlinegdb.com/eA87wKQkU
#include <stdio.h>
#include <stdint.h>
int main()
{
uint64_t A = -1, B = 1;
if ((signed)A < (signed)B)
{
printf("signed comparison");
}
return 0;
}
To ensure an overall signed comparison, looks like the (signed) type modifier must be applied to A and B.
Is this correct?
Also, I haven't seen any C code using ((signed)A < (signed)B) and was wondering if it's valid C89/99?
Perhaps ((int64_t)A < (int64_t)B) is a better approach?
Thanks.

The answer to both questions is yes:
if you only convert A or B as (signed), which means (signed int), the comparison will still be performed as uint64_t because the converted value will be converted to the larger type uint64_t. Converting both A and B is hence necessary.
converting to int64_t is probably a better idea as this signed type is larger, but it should not matter in this particular example: converting A, whose value is UINT64_MAX to int or int64_t is implementation defined and may or may not produce -1. The C Standard allows for an implementation defined signal to be raised by this out of range conversion.
on most current architectures, no signal will be raised and the conversion of A will indeed produce -1 and the code will print signed comparison. Yet you should end the output with a newline for proper operation.

This is a slightly unusual solution, irrelevant for all practical purposes, but it does have the one advantage of avoiding the following conversion rule from the C standard:
Otherwise, the new type is signed and the value cannot be represented in it; either the
result is implementation-defined or an implementation-defined signal is raised.
The idea of this solution is to invert the top bit of the unsigned type in the values being compared before the comparison.
Converting -1 to uint64_t produces the value 0xffffffffffffffff in A.
Converting 1 to uint64_t produces the value 0x0000000000000001 in B.
So the comparison A < B is false.
Denoting A and B with the top bit inverted as Ax and Bx respectively, then Ax has the value 0x7fffffffffffffff and Bx has the value 0x8000000000000001. The comparison Ax < Bx is true.
One way to invert the top bit of a uint64_t value is to add or subtract INT64_MIN and convert the result back to uint64_t. Converting INT64_MIN to uint64_t produces the value 0x8000000000000000 and adding or subtracting that to another uint64_t value will invert the top bit. This also works for any other exact-width unsigned type with the 64 changed to exact width in question.
So the following will do a "signed comparison" of the uint64_t values A and B:
if ((uint64_t)(A - INT64_MIN) < (uint64_t)(B - INT64_MIN))
printf("A signed less than B\n");
Type-casting the adjusted values back to the unsigned integer type as shown above is only necessary for unsigned types whose values can all be represented by int. For example, it is necessary when A and B are of type uint8_t. A would have the value 255, B would have the value 1, Ax would have value 383 (for subtraction) or 127 (for addition), Bx would have the value 129 (for subtraction) or -127 (for addition), and Ax < Bx would be false. The type-cast would convert Ax to 127 and Bx to 129 so the comparison Ax < Bx would be true.

Related

Unsigned integer underflow in C

I've seen multiple questions on the site addressing unsigned integer overflow/underflow.
Most of the questions about underflow ask about assigning a negative number to an unsigned integer; what's unclear to me is what happens when an unsigned int is subtracted from another unsigned int e.g. a - b where the result is negative. The relevant part of the standard is:
A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
In this context how do you interpret "reduced"? Does it mean that UINT_MAX+1 is added to the negative result until it is >= 0?
I see that the main point is addressed by this question (which basically says that the standard chooses to talk about overflow but the main point about modulo holds for underflow too) but it's still unclear to me:
Say the result of a-b is -1; According to the standard, the operation -1%(UINT_MAX+1) will return -1 (as is explained here); so we're back to where we started.
This may be overly pedantic, but does this modulo mean a mathematical modulo as opposed to C's computational modulo?
Firstly, a result that is below the minimum value of the given integer type is not called "underflow" in C. The term "underflow" is reserved for floating-point types and means something completely different. Going out of range of an integer type is always overflow, regardless of which end of the range you cross. So the fact that you don't see the language specification talking about "underflow" doers not really mean anything in this case.
Secondly, you are absolutely right about the meaning of the word "reduced". The final value is defined by adding (or subtracting) UINT_MAX+1 from the "mathematical" result until it returns into the range of unsigned int. This is also the same thing as Euclidean "modulo" operation.
The part of the standard you posted talks about overflow, not underflow.
"Does it mean that UINT_MAX+1 is added to the negative result until it is >= 0?"
You can think that's what happens. Abstractly the result will be the same. A similar question has already been asked about it. Check this link: Question about C behaviour for unsigned integer underflow for more details.
Another way to think is that, for example, -1 is in principle from type int (that is 4 bytes, in which all bits are 1). Then, when you tell the program to interpret all these bits 1 as unsigned int, its value will be interpreted as UINT_MAX.
Under the hood, addition, or subtraction is bit wise and sign independent. The code generated could use the same instructions independent of whether it is signed or not. It is other operators that interpret the result, for example a > 0. Do the bit wise add or sub and this tells you the answer. b0 - b1 = b111111111 the answer is the same independent of the sign. It is only other operators that see the answer as -1 for signed types and 0xFF for unsigned types. The standard describes this behaviour, but I always find it easiest to remember how it works and deduce the consequences to the code I am writing.
signed int adds(signed int a, signed int b)
{
return a + b;
}
unsigned int addu(unsigned a, unsigned b)
{
return a + b;
}
int main() {
return 0;
}
->
adds(int, int):
lea eax, [rdi+rsi]
ret
addu(unsigned int, unsigned int):
lea eax, [rdi+rsi]
ret
main:
xor eax, eax
ret

What is the difference between literals and variables in C (signed vs unsigned short ints)?

I have seen the following code in the book Computer Systems: A Programmer's Perspective, 2/E. This works well and creates the desired output. The output can be explained by the difference of signed and unsigned representations.
#include<stdio.h>
int main() {
if (-1 < 0u) {
printf("-1 < 0u\n");
}
else {
printf("-1 >= 0u\n");
}
return 0;
}
The code above yields -1 >= 0u, however, the following code which shall be the same as above, does not! In other words,
#include <stdio.h>
int main() {
unsigned short u = 0u;
short x = -1;
if (x < u)
printf("-1 < 0u\n");
else
printf("-1 >= 0u\n");
return 0;
}
yields -1 < 0u. Why this happened? I cannot explain this.
Note that I have seen similar questions like this, but they do not help.
PS. As #Abhineet said, the dilemma can be solved by changing short to int. However, how can one explains this phenomena? In other words, -1 in 4 bytes is 0xff ff ff ff and in 2 bytes is 0xff ff. Given them as 2s-complement which are interpreted as unsigned, they have corresponding values of 4294967295 and 65535. They both are not less than 0 and I think in both cases, the output needs to be -1 >= 0u, i.e. x >= u.
A sample output for it on a little endian Intel system:
For short:
-1 < 0u
u =
00 00
x =
ff ff
For int:
-1 >= 0u
u =
00 00 00 00
x =
ff ff ff ff
The code above yields -1 >= 0u
All integer literals (numeric constansts) have a type and therefore also a signedness. By default, they are of type int which is signed. When you append the u suffix, you turn the literal into unsigned int.
For any C expression where you have one operand which is signed and one which is unsiged, the rule of balacing (formally: the usual arithmetic conversions) implicitly converts the signed type to unsigned.
Conversion from signed to unsigned is well-defined (6.3.1.3):
Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or
subtracting one more than the maximum value that can be represented in the new type
until the value is in the range of the new type.
For example, for 32 bit integers on a standard two's complement system, the max value of an unsigned integer is 2^32 - 1 (4294967295, UINT_MAX in limits.h). One more than the maximum value is 2^32. And -1 + 2^32 = 4294967295, so the literal -1 is converted to an unsigned int with the value 4294967295. Which is larger than 0.
When you switch types to short however, you end up with a small integer type. This is the difference between the two examples. Whenever a small integer type is part of an expression, the integer promotion rule implicitly converts it to a larger int (6.3.1.1):
If an int can represent all values of the original type (as restricted
by the width, for a bit-field), the value is converted to an int;
otherwise, it is converted to an unsigned int. These are called the
integer promotions. All other types are unchanged by the integer
promotions.
If short is smaller than int on the given platform (as is the case on 32 and 64 bit systems), any short or unsigned short will therefore always get converted to int, because they can fit inside one.
So for the expression if (x < u), you actually end up with if((int)x < (int)u) which behaves as expected (-1 is lesser than 0).
You're running into C's integer promotion rules.
Operators on types smaller than int automatically promote their operands to int or unsigned int. See comments for more detailed explanations. There is a further step for binary (two-operand) operators if the types still don't match after that (e.g. unsigned int vs. int). I won't try to summarize the rules in more detail than that. See Lundin's answer.
This blog post covers this in more detail, with a similar example to yours: signed and unsigned char. It quotes the C99 spec:
If an int can represent all values of the original type, the value is
converted to an int; otherwise, it is converted to an unsigned int.
These are called the integer promotions. All other types are unchanged
by the integer promotions.
You can play around with this more easily on something like godbolt, with a function that returns one or zero. Just look at the compiler output to see what ends up happening.
#define mytype short
int main() {
unsigned mytype u = 0u;
mytype x = -1;
return (x < u);
}
Other than what you seem to assume , this is not a property of the particular width of the types, here 2 byte versus 4 bytes, but a question of the rules that are to be applied. The integer promotion rules state that short and unsigned short are converted to int on all platforms where the corresponding range of values fit into int. Since this is the case here, both values are preserved and obtain the type int. -1 is perfectly representable in int as is 0. So the test results in -1 is smaller than 0.
In the case of testing -1 against 0u the common conversion choses the unsigned type as a common type to which both are converted. -1 converted to unsigned is the value UINT_MAX, which is larger than 0u.
This is a good example, why you should never use "narrow" types to do arithmetic or comparison. Only use them if you have a sever size constraint. This will rarely be the case for simple variables, but mostly for large arrays where you can really gain from storing in a narrow type.
0u is not unsigned short, it's unsigned int.
Edit:: The explanation to the behavior,
How comparison is performed ?
As answered by Jens Gustedt,
This is called "usual arithmetic conversions" by the standard and
applies whenever two different integer types occur as operands of the
same operator.
In essence what is does
if the types have different width (more precisely what the standard
calls conversion rank) then it converts to the wider type if both
types are of same width, besides really weird architectures, the
unsigned of them wins Signed to unsigned conversion of the value -1
with whatever type always results in the highest representable value
of the unsigned type.
The more explanatory blog written by him could be found here.

Subtracting 0x8000 from an int

I am reverse engineering some old C, running under Win95 (yes, in production) appears to have been compiled with a Borland compiler (I don't have the tool chain).
There is a function which does (among other things) something like this:
static void unknown(int *value)
{
int v = *value;
v-=0x8000;
*value = v;
}
I can't quite work out what this does. I assume 'int' in this context is signed 32 bit. I think 0x8000 would be unsigned 32bit int, and outside the range of a signed 32 bit int. (edit - this is wrong, it is outside of a signed 16 bit int)
I am not sure if one of these would be cast first, and how the casting would handle overflows, and/or how the subtraction would handle the over flow.
I could try on a modern system, but I am also unsure if the results would be the same.
Edit for clarity:
1: 'v-=0x8000;' is straight from the original code, this is what makes little sense to me. v is defined as an int.
2: I have the code, this is not from asm.
3: The original code is very, very bad.
Edit: I have the answer! The answer below wasn't quite right, but it got me there (fix up and I'll mark it as the answer).
The data in v is coming from an ambiguous source, which actually seems to be sending unsigned 16 bit data, but it is being stored as a signed int. Latter on in the program all values are converted to floats and normalised to an average 0 point, so actual value doesn't matter, only order. Because we are looking at an unsigned int as a signed one, values over 32767 are incorrectly placed below 0, so this hack leaves the value as signed, but swaps the negative and positive numbers around (not changing order). End results is all numbers have the same order (but different values) as if they were unsigned in the first place.
(...and this is not the worst code example in this program)
In Borland C 3.x, int and short were the same: 16 bits. long was 32-bits.
A hex literal has the first type in which the value can be represented: int, unsigned int, long int or unsigned long int.
In the case of Borland C, 0x8000 is a decimal value of 32768 and won't fit in an int, but will in an unsigned int. So unsigned int it is.
The statement v -= 0x8000 ; is identical to v = v - 0x8000 ;
On the right-hand side, the int value v is implicitly cast to unsigned int, per the rules, the arithmetic operation is performed, yielding an rval that is an unsigned int. That unsigned int is then, again per the rules, implicitly cast back to the type of the lval.
So, by my estimation, the net effect is to toggle the sign bit — something that could be more easily and clearly done via simple bit-twiddling: *value ^= 0x8000 ;.
There is possibly a clue on this page http://www.ousob.com/ng/borcpp/nga0e24.php - Guide to Borland C++ 2.x ( with Turbo C )
There is no such thing as a negative numeric constant. If
a minus sign precedes a numeric constant it is treated as
the unary minus operator, which, along with the constant,
constitutes a numeric expression. This is important with
-32768, which, while it can be represented as an int,
actually has type long int, since 32768 has type long. To
get the desired result, you could use (int) -32768,
0x8000, or 0177777.
This implies the use of two's complement for negative numbers. Interestingly, the two's complement of 0x8000 is 0x8000 itself (as the value +32768 does not fit in the range for signed 2 byte ints).
So what does this mean for your function? Bit wise, this has the effect of toggling the sign bit, here are some examples:
f(0) = f(0x0000) = 0x8000 = -32768
f(1) = f(0x0001) = 0x8001 = -32767
f(0x8000) = 0
f(0x7fff) = 0xffff
It seems like this could be represented as val ^= 0x8000, but perhaps the XOR operator was not implemented in Borland back then?

why the result is "2" of unsigned int (1) - unsigned int (0xFFFFFFFF)

Please look at the following codes:
#include <stdlib.h>
#include <stdio.h>
int main()
{
unsigned int a = 1;
unsigned int b = -1;
printf("0x%X\n", (a-b));
return 0;
}
The result is 0x2.
I think the integer promotion should not happen because the type of both of "a" and "b" are unsigned int. But the result beats me.... I don't know the reason.
By the way, I know the arithmetic result should be 2 because 1-(-1)=2. But the type of b is unsigned int. When assign the (-1) to b, the value of b is 0xFFFFFFFF actually. It is the maximum value of unsigned int. When one small unsigned value subtract one big value, the result is not that I expect.
From the answer below, I think maybe the overflow is a good explanation。
Now I writes other test codes. It proves the overflow answer is right.
#include <stdlib.h>
#include <stdio.h>
int main()
{
unsigned int c = 1;
unsigned int d = -1;
printf("0x%llx\n", (unsigned long long)c-(unsigned long long)d);
return 0;
}
The result is "0xffffffff00000002". It is I expect.
unsigned int a = 1;
This initializes a to 1. Actually, since 1 is of type int, there's an implicit int-to-unsigned conversion, but it's a trivial conversion that doesn't change the value or representation).
unsigned int b = -1;
This is more interesting. -1 is of type int, and the initialization implicitly converts it from int to unsigned int. Since the mathematical value -1 cannot be represented as an unsigned int, the conversion is non-trivial. The rule in this case is (quoting section 6.3.1.3 of the C standard):
the value is converted by repeatedly adding or subtracting one more
than the maximum value that can be represented in the new type until
the value is in the range of the new type.
Of course it doesn't actually have to do it that way, as long as the result is the same. Adding UINT_MAX+1 ("one more than the maximum value that can be represented in the new type") to -1 yields UINT_MAX. (That addition is defined mathematically; it's not itself subject to any type conversions.)
In fact, assigning -1 to an object of any unsigned type is a good way to get the maximum value of that type without having to refer to the *_MAX macros defined in <limits.h>.
So, assuming a typical 32-bit system, we have a == 1 and b == 0xFFFFFFFF.
printf("0x%X\n", (a-b));
The mathematical result of the subtraction is -0xFFFFFFFE, but that's obviously outside the range of unsigned int. The rules for unsigned arithmetic are similar to the rules for unsigned conversion; the result is 2.
Who says you're suffering integer promotion? Let's pretend that your integers are two's complement (likely, though not mandated by the standard) and they're only four bits wide (not possible according to the standard, I'm just using this to simplify things).
int unsigned-int bit-pattern
--- ------------ -----------
1 1 0001
-1 15 1111
------
(subtract with borrow) 1 0010
(only have four bits) 0010 -> 2
You can see here that you can get the result you see without any promotion to signed or wider types.
There should be a compiler warning that you probably ignored or turned off, but it's still possible to store -1 in an unsigned integer. Internally, -1 is stored on a 32-bit machine as 0xffffffff. So if you subtract 0xffffffff from 1, you end up with -0xfffffffe, which is 2. (There are no negative numbers, a negative number is the maximum integer value plus one minus the number).
Bottom line - signed or unsigned doesn't matter at all, it only comes to play when you compare values.
And mathematically speaking, 1 - (-1) = 1+1.
If you subtract a negative number, it is the equivalent of adding a positive number.
a = 1
b = -1
(a-b) = ?
((1)-(-1)) = ?
(1-(-1)) = ?
(1+1) = ?
2 = ?
At first you might think that this isn't allowed, since you specified an unsigned int; however, you are also converting signed int (the -1 constant) to an unsigned int. So, you are effectively storing the exact same bits into the unsigned int (0xFFFF).
Then, in the expression, you take the negative of the 0xFFFF value, which of course forces the number to be signed. In effect, you are circumventing the unsigned directive at ever step.

Adding unsigned integers in C

Here are two very simple programs. I would expect to get the same output, but I don't. I can't figure out why. The first outputs 251. The second outputs -5. I can understand why the 251. However, I don't see why the second program gives me a -5.
PROGRAM 1:
#include <stdio.h>
int main()
{
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b= -5;
c = (a + b);
printf("c hex: %x\n", c);
printf("c dec: %d\n",c);
}
Output:
c hex: fb
c dec: 251
PROGRAM 2:
#include <stdio.h>
int main()
{
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b= 5;
c = (a - b);
printf("c hex: %x\n", c);
printf("c dec: %d\n",c);
}
Output:
c hex: fffffffb
c dec: -5
In the first program, b=-5; assigns 251 to b. (Conversions to an unsigned type always reduce the value modulo one plus the max value of the destination type.)
In the second program, b=5; simply assigns 5 to b, then c = (a - b); performs the subtraction 0-5 as type int due to the default promotions - put simply, "smaller than int" types are always promoted to int before being used as operands of arithmetic and bitwise operators.
Edit: One thing I missed: Since c has type unsigned int, the result -5 in the second program will be converted to unsigned int when the assignment to c is performed, resulting in UINT_MAX-4. This is what you see with the %x specifier to printf. When printing c with %d, you get undefined behavior, because %d expects a (signed) int argument and you passed an unsigned int argument with a value that's not representable in plain (signed) int.
There are two separate issues here. The first is the fact that you are getting different hex values for what looks like the same operations. The underlying fact that you are missing is that chars are promoted to ints (as are shorts) to do arithmetic. Here is the difference:
a = 0 //0x00
b = -5 //0xfb
c = (int)a + (int)b
Here, a is extended to 0x00000000 and b is extended to 0x000000fb (not sign extended, because it is an unsigned char). Then, the addition is performed, and we get 0x000000fb.
a = 0 //0x00
b = 5 //0x05
c = (int)a - (int)b
Here, a is extended to 0x00000000 and b is extended to 0x00000005. Then, the subtraction is performed, and we get 0xfffffffb.
The solution? Stick with chars or ints; mixing them can cause things you won't expect.
The second problem is that an unsigned int is being printed as -5, clearly a signed value. However, in the string, you told printf to print its second argument, interpreted as a signed int (that's what "%d" means). The trick here is that printf doesn't know what the types of the variables you passed in. It merely interprets them in the way the string tells it to. Here's an example where we tell printf to print a pointer as an int:
int main()
{
int a = 0;
int *p = &a;
printf("%d\n", p);
}
When I run this program, I get a different value each time, which is the memory location of a, converted to base 10. You may note that this kind of thing causes a warning. You should read all of the warnings your compiler gives you, and only ignore them if you're completely sure you are doing what you intend to.
You're using the format specifier %d. That treats the argument as a signed decimal number (basically int).
You get 251 from the first program because (unsigned char)-5 is 251 then you print it like a signed decimal digit. It gets promoted to 4 bytes instead of 1, and those bits are 0, so the number looks like 0000...251 (where the 251 is binary, I just didn't convert it).
You get -5 from the second program because (unsigned int)-5 is some large value, but casted to an int, it's -5. It gets treated like an int because of the way you use printf.
Use the format specifier %ud to print unsigned decimal values.
What you're seeing is the result of how the underlying machine is representing the numbers how the C standard defines signed to unsigned type conversions (for the arithmetic) and how the underlying machine is representing numbers (for the result of the undefined behavior at the end).
When I originally wrote my response I had assumed that the C standard didn't explicitly define how signed values should be converted to unsigned values, since the standard doesn't define how signed values should be represented or how to convert unsigned values to signed values when the range is outside that of the signed type.
However, it turns out that the standard does explicitly define that when converting from negative signed to positive unsigned values. In the case of an integer, a negative signed value x will be converted to UINT_MAX+1-x, just as if it were stored as a signed value in two's complement and then interpreted as an unsigned value.
So when you say:
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b = -5;
c = a + b;
b's value becomes 251, because -5 is converted to an unsigned type of value UCHAR_MAX-5+1 (255-5+1) using the C standard. It's then after that conversion that the addition takes place. That makes a+b the same as 0 + 251, which is then stored in c. However, when you say:
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b = 5;
c = (a-b);
printf("c dec: %d\n", c);
In this case, a and b are promoted to unsigned ints, to match with c, so they remain 0 and 5 in value. However 0 - 5 in unsigned integer math leads to an underflow error, which is defined to result in UINT_MAX+1-5. If this had happened before the promotion, the value would be UCHAR_MAX+1-5 (i.e. 251 again).
However, the reason you see -5 printed in your output is a combination of the fact that the unsigned integer UINT_MAX-4 and -5 have the same exact binary representation, just like -5 and 251 do with a single-byte datatype, and the fact that when you used "%d" as the formatting string, that told printf to interpret the value of c as a signed integer instead of an unsigned integer.
Since a conversion from unsigned values to signed values for invalid values isn't defined, the result becomes implementation specific. In your case, since the underlying machine uses two's complement for signed values, the result is that the unsigned value UINT_MAX-4 becomes the signed value -5.
The only reason this doesn't happen in the first program because an unsigned int and a signed int can both represent 251, so converting between the two is well defined and using "%d" or "%u" doesn't matter. In the second program, however, it results in undefined behavior and becomes implementation specific since your value of UINT_MAX-4 went outside the range of an signed int.
What's happening under the hood
It's always good to double check what you think is happening or what should happen with what's actually happening, so let's look at the assembly language output from the compiler now to see exactly what's going on. Here's the meaningful part of the first program:
mov BYTE PTR [rbp-1], 0 ; a becomes 0
mov BYTE PTR [rbp-2], -5 ; b becomes -5, which as an unsigned char is also 251
movzx edx, BYTE PTR [rbp-1] ; promote a by zero-extending to an unsigned int, which is now 0
movzx eax, BYTE PTR [rbp-2] ; promote b by zero-extending to an unsigned int which is now 251
add eax, edx ; add a and b, that is, 0 and 251
Notice that although we store a signed value of -5 in the byte b, when the compiler promotes it, it promotes it by zero-extending the number, meaning it's being interpreted as the unsigned value that 11111011 represents instead of the signed value. Then the promoted values are added together to become c. This is also why the C standard defines signed to unsigned conversions the way it does -- it's easy to implement the conversions on architectures that use two's complement for signed values.
Now with program 2:
mov BYTE PTR [rbp-1], 0 ; a = 0
mov BYTE PTR [rbp-2], 5 ; b = 5
movzx edx, BYTE PTR [rbp-1] ; a is promoted to 32-bit integer with value 0
movzx eax, BYTE PTR [rbp-2] ; b is promoted to a 32-bit integer with value 5
mov ecx, edx
sub ecx, eax ; a - b is now done as 32-bit integers resulting in -5, which is '4294967291' when interpreted as unsigned
We see that a and b are once again promoted before any arithmetic, so we end up subtracting two unsigned ints, which leads to a UINT_MAX-4 due to underflow, which is also -5 as a signed value. So whether you interpret it as a signed or unsigned subtraction, due to the machine using two's complement form, the result matches the C standard without any extra conversions.
Assigning a negative number to an unsigned variable is basically breaking the rules. What you're doing is converting the negative number to a large positive number. You're not even guaranteed, technically, that the conversion is the same from one processor to another -- on a 1's complement system (if any still existed) you'd get a different value, eg.
So you get what you get. You can't expect signed algebra to still apply.

Resources