Why is char type considered unsigned in ARM architecture?

I was recently surprised to find Clang thinks the char type is considered unsigned in ARM architecture. For the sample code:

#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include <iostream>

using namespace clang;
using namespace clang::ast_matchers;
using namespace clang::tooling;
using namespace std;

static llvm::cl::OptionCategory MatchMatcherCategory("Options");

class MatchAlert : public MatchFinder::MatchCallback
   virtual void run(const MatchFinder::MatchResult &Result)
      const VarDecl *VD = Result.Nodes.getNodeAs<VarDecl>("varDecl");
      string var = VD->getNameAsString();
      QualType QT = VD->getType();
      if (QT->isCharType())
         cout << var << " is a char\n";
      if (QT->isUnsignedIntegerType())
         cout << var << " is an unsigned integer type\n";

int main(int argc, const char** argv)
   MatchFinder Finder;
   MatchAlert varDeclAlert;

   CommonOptionsParser OptionsParser(argc, argv, MatchMatcherCategory);
   ClangTool Tool(OptionsParser.getCompilations(),

   Finder.addMatcher(varDecl().bind("varDecl"), &varDeclAlert);

and the input file c.cpp:

char ch;
unsigned char unsignedCh;

when I run:

$ ./test c.cpp --
ch is a char
unsignedCh is a char
unsignedCh is an unsigned integer type

as expected. But when I run it with:

$ ./test --extra-arg=--target=arm-none-eabi c.cpp --
ch is a char
ch is an unsigned integer type
unsignedCh is a char
unsignedCh is an unsigned integer type

So my question is why? I tried it with several other architectures, and always see char as a signed value.

I’m running on an Ubuntu Linux system with:

$ ./test -version
LLVM version 12.0.1
Optimized build.
Default target: x86_64-unknown-linux-gnu
Host CPU: skylake

Indeed, plain char is unsigned on ARM (contrary to e.g. x86): Documentation – Arm Developer

My understanding is that this derives from earlier Arm Architecture versions (pre Arm v4) which only had a LDRB instruction that loaded an unsigned byte. This made loading a signed byte into a 32-bit register more expensive than an unsigned byte. Arm v4 introduced LDRSB which made this irrelevant but historical precedent means that char remains unsigned in the ABI.