"Portable assembly language" is an oxymoron. And I have never heard anyone use that phrase to describe C.
A quick web search will solve that for you. The phrase has been bandied about for quite a few years now, although many people disagree on the topic.
Never mind building abstractions. The C language itself is a significant abstraction from the machine level. Only a small handful of operators and constructs in C have a close analogue to assembler statements (e.g., accumulation, shift and bitwise logical operators.) Therefore I maintain that it is not a low-level language.
While I certainly agree that C is a significant (and useful) abstraction from the machine -- or more specifically, the assembler -- level, I think you've glossed over quite a few things that are much closer in C to how it works at the machine code level, compared to most other languages. Some that spring to mind are:
- Pointers and pointer arithmetic -- and not just for fun; you need to use them to get things done.
- Array indexing -- this is little more than syntax sugar atop pointer arithmetic. Indexing element i of an array is equivalent to referencing *(array_base + sizeof that type * i), and accordingly there is no bounds checking etc.
- No real strings -- just arrays of characters which suffer from the same problems above.
- No garbage collection or even refcounting -- if you want to return a string value from a function, you need to either take a buffer (and, ideally, the size of the buffer since you can't automatically determine it) and store the string there, or you need to dynamically allocate memory and return a pointer. Whoever called the function then needs to "free" that pointer later. That's some pretty low-level stuff you have to do just to pass some strings around!
These are just a handful of many things you need to live with in assembly AND in C, unless you use some non-universal, non-standard library for e.g. strings or GC (Hans Boehm's drop in one seems pretty good though). Additionally, C forces you to declare the types of each variable and function, yet cannot properly enforce type-safety and has no type inference which would have made the job easier.
C is undeniably less low-level than assembly, I consider C to be a low-level language in an arbitrary line-in-the-sand sense. Of course, my view on this is no more valid than yours since how we define "low-level" as some absolute marker is pretty subjective!