If I say anything wrong, please correct me.
Your problem have a flag with "undefined-behavior". I think it's not right.
If you have any doubts about the program, I suggest looking at the disassembly code of the program. All your confusion may be easily resolved by examining it.
The output:
-2 254
-2 254
It is right and it's centain behavior. This behavior is determined by the C language itself or the C language standard.
The key to outputting depends on how the programmer wants to interpret the stored value of FE.If you see FF as a unsigned char, it's 255(or FFFF as a unsigned short it's 65535 or FFFFFFFF as a unsigned int it's 4294967295). And see FF as a signed char, it's -1(or FFFF as a signed short it's -1 or FFFFFFFF as a signed int it's -1).
The same as you see FE as a unsigned char, it's 254. And see FE as a signed char, it's -2. And so on ......
When you ask a computer to store -2 and 254, the computer doesn't recognize positive or negative numbers, it only recognizes 0(In circuitry, it could perhaps be said to be "disconnected" or "broken.") and 1(In circuitry, it could perhaps be said to be "closed" or "connected."). If you ask the computer to store -2, it will store FE(Because of variable c and variable d is type of char,it occupy 1 byte) somewhere in memory(As @David C. Rankin point out that on computers that encode negative signed values in two-compliment.). Similarly, if you ask it to store 254, it will also store FE somewhere in memory.
See below code:
#include <stdio.h>
int main()
{
signed char c;
unsigned char d;
c = (signed char) 0xFE;
d = (unsigned char) c;
printf("%d %d\n", c, d);
d = (unsigned char)0xFE;
c = (signed char) d;
printf("%d %d\n", c, d);
return 0;
}
Run it with below command:
clang -Wall -Wextra -pedantic -std=c89 foo.c && ./a.out
will output:
-2 254
-2 254
Why output double -2 254?
There is no -2 and 254 in the code.
It seems that only the number 0xFF was observed.
c = (signed char) 0xFE;
d = (unsigned char)0xFE;
So where does -2 and 254 come from?
Simple explanation: (Below have a more detailed explanation)

We find thatvariable c and variable d is char type, but %d is output int(or signed int) , how should compiler proceed now? The answer is signed extension and unsigned extension .
So now the value 0xFE stored in variable c has been transformed to 0xFFFFFFFE through an sign extension, and the value 0xFE stored in variable d has been transformed to 0x000000FE through an zero extension. When 0xFFFFFFFE printed is -2 with %d, and 0x000000FE printed is 254 with %d.(Are you not quite familiar with or don't quite understand 0xFFFFFFFE? Let's continue reading, as there's an explanation below.)
Or code like below:
#include <stdio.h>
int main()
{
signed char c;
unsigned char d;
c = (signed char) 254;
d = (unsigned char) c;
printf("%d %d\n", c, d);
d = (unsigned char)254;
c = (signed char) d;
printf("%d %d\n", c, d);
return 0;
}
Run it with below command:
clang -Wall -Wextra -pedantic -std=c89 foo.c && ./a.out
will output:
-2 254
-2 254
In order to better explain your confusion, please take a look at the following code.
#include <stdio.h>
int main()
{
signed char c;
unsigned char d;
c = (signed char) -2;
d = (unsigned char) c;
printf("%d %d %u %u\n", c, d, c, d);
d = (unsigned char) 254;
c = (signed char) d;
printf("%d %d %u %u\n", c, d, c, d);
return 0;
}
Run it with below command:
clang -Wall -Wextra -pedantic -std=c89 foo.c && ./a.out
will output:
-2 254 4294967294 254
-2 254 4294967294 254
Or run it with below command:
gcc -g -o foo foo.c && ./foo
will output:
-2 254 4294967294 254
-2 254 4294967294 254
Output is right.
More details explanation:

We find that variable c or variable d is char type, but %u is output unsigned int , how should compiler proceed now? The answer is signed extension and unsigned extension .
When we examine the disassembly code, we do indeed discover sign extension and zero extension. See below picture:

The other picture:

We found that use char type(BYTE) when assign value to variable c and variable d, but at printf the value of variable c and variable d before, there are some instruction like:
movzx esi,BYTE PTR [rbp-0x1]
movsx ecx,BYTE PTR [rbp-0x2]
movzx edx,BYTE PTR [rbp-0x1]
movsx eax,BYTE PTR [rbp-0x2]
movzx is zero extension, and movsx is sign extension. Like esi,ecx,edx,eax is equal to int(ecx occupy 4 byte, the type of int also occupy 4 byte).
So now the value 0xFE stored in variable c has been transformed to 0xFFFFFFFE(saved in ecx or eax) through an sign extension, and the value 0xFE stored in variable d has been transformed to 0x000000FE(saved in esi or edx) through an zero extension. When 0xFFFFFFFE printed is 4294967294 with %u, 0xFFFFFFFE printed is -2 with %d , and 0x000000FE printed is 254 with %u, 0x000000FE printed is 254 with %d.
The representation of 4294967294 see below picture.

The representation of -2 see below picture.

So now you see that when outputting the value of variable c or variable d, using %d and %u to print them out will yield different results. However, both representations refer to the same value stored in memory. The key point is how you choose to interpret the value of c or d.