0

I'm learning convert C to assembly, then I found char and short data are stored in 4 byte registers.

note: I use -Og -g to compiler C, and use gdb disas main! In addition, my computer is 64bit.

Below is code about char and correspond to assembly(I think short and char are same problem, so I put one of two code):

#include <stdio.h>

int main(void) {
    const int LEN = 3;
    char c[LEN];
    c[0] = 1;
    c[1] = 2;
    c[2] = 3;

    for(int i = 0; i < LEN; i ++) {
        printf("%d\n", c[i]);
    }

    return 0;
}

a part of disassembler code!

   0x000000000000116e <+5>:     sub    $0x10,%rsp
   0x0000000000001172 <+9>:     mov    %fs:0x28,%rax
   0x000000000000117b <+18>:    mov    %rax,0x8(%rsp)
   0x0000000000001180 <+23>:    xor    %eax,%eax
   0x0000000000001182 <+25>:    movb   $0x1,0x5(%rsp)
   0x0000000000001187 <+30>:    movb   $0x2,0x6(%rsp)
   0x000000000000118c <+35>:    movb   $0x3,0x7(%rsp)
   0x0000000000001191 <+40>:    mov    $0x0,%ebx
   0x0000000000001196 <+45>:    jmp    0x11b9 <main+80>
   0x0000000000001198 <+47>:    movslq %ebx,%rax

   # why %edx?
   0x000000000000119b <+50>:    movsbl 0x5(%rsp,%rax,1),%edx


   0x00000000000011a0 <+55>:    lea    0xe5d(%rip),%rsi        # 0x2004
   0x00000000000011a7 <+62>:    mov    $0x1,%edi
   0x00000000000011ac <+67>:    mov    $0x0,%eax
   0x00000000000011b1 <+72>:    callq  0x1070 <__printf_chk@plt>
   0x00000000000011b6 <+77>:    add    $0x1,%ebx
   0x00000000000011b9 <+80>:    cmp    $0x2,%ebx
   0x00000000000011bc <+83>:    jle    0x1198 <main+47>

I have learnt a little about java data types, hmm, like byte, char, or short is promoted to int. I'm not sure they are something related.

OnlyWick
  • 342
  • 2
  • 10
  • 2
    See https://en.cppreference.com/w/c/language/conversion especially the sections on default argument promotion and integer promotion. – Shawn Jul 11 '22 at 17:27
  • 4
    The language promotes char and short expressions to int automatically and immediately -- for example, there is no char arithmetic in the language that is separate from int arithmetic: c[i] + c[i+1] would be done using int arithmetic despite the sources being char. So, the value you're passing to printf, c[i] is an int before printf ever sees it. – Erik Eidt Jul 11 '22 at 17:38
  • Even when int promotions aren't required, the compiler still typically uses 32 bit registers for everything because they can sometimes be more efficient and are pretty much never less efficient. An 8 bit register on x86 is a portion of a 32 bit register, so when an 8 bit register is modified, it has to be merged with the remainder of the register. – prl Jul 11 '22 at 17:39
  • 1
    And the ABI requires int's in registers to be properly extended to 64-bits, which is accomplished with that movsbl to edx: doing that makes 64-bit rdx properly set for the int. – Erik Eidt Jul 11 '22 at 17:41
  • 1
    @Shawn thanks, I find `If int can represent the entire range of values of the original type (or the range of values of the original bit field), the value is converted to type int. Otherwise the value is converted to unsigned int.` – OnlyWick Jul 11 '22 at 17:43
  • @ErikEidt cool, but why `char` and `short` need the promotion? cos the cpu reads the size of data at once? – OnlyWick Jul 11 '22 at 17:51
  • @ErikEidt you mean `Sys V ABI`? – OnlyWick Jul 11 '22 at 17:52
  • @OnlyWick "but why char and short need the promotion? " --> because the C language is defined that way - certainly for simplicity/efficiency of code generation. – chux - Reinstate Monica Jul 11 '22 at 17:55
  • @chux-ReinstateMonica ohh, I see, it seems that I think too much. hhhh – OnlyWick Jul 11 '22 at 17:58
  • 1
    @OnlyWick, "Why char and short data are stored in 4 byte registers?" is amiss. A `char` and `short` commonly take 1, 2 bytes. When those are passed as a ... argument, they are read, converted to an `int` and so that result is saved as 4 bytes. – chux - Reinstate Monica Jul 11 '22 at 18:00
  • @chux cool, thanks! I just testd it! – OnlyWick Jul 11 '22 at 18:10
  • At the assembly level, it's more efficient to deal with full registers than half ones. – Michael Chourdakis Jul 11 '22 at 19:28

2 Answers2

1

With the %d format you specify that an "int" is to be printed, thus the value needs to get loaded to (at least) an int-sized register.

SoronelHaetir
  • 14,104
  • 1
  • 12
  • 23
  • %f is for doubles... – Shawn Jul 11 '22 at 17:25
  • change the format option is not change it use %edx – OnlyWick Jul 11 '22 at 17:30
  • @OnlyWick: The other reason is [Why doesn't GCC use partial registers?](https://stackoverflow.com/q/41573502), which applies even when correctness doesn't require extending to 32-bit or 64-bit. – Peter Cordes Jul 11 '22 at 22:25
  • @Sorone: Promotion of `c[i]` to `int` doesn't actually depend on the format string, just that it's being passed to a variadic function. (Thus the [*default argument promotions*](https://en.cppreference.com/w/c/language/conversion) apply.) The default integer promotions are the reason it's legal to use `%d` for `c[i]`, and why `%c` actually accepts an `int` and is defined as converting it to `char`. And why `%f` prints a `double`, because it's impossible to pass a `float` as a variadic argument in C. – Peter Cordes Jul 12 '22 at 04:15
  • I guess in theory a compiler could look at the format string and leave high garbage above a byte, if the format string used `%c` for that argument. That would be valid per the as-if rule. But in practice compilers follow the ABI's calling convention rules fully, if they emit a call to `printf` at all (rather than to `puts` with just a constant string.) – Peter Cordes Jul 12 '22 at 04:17
0

When you merely reference a char or short variable in an expression, the language rules say that it is immediately promoted to int.  So, given char c, d; if we say c + d this is the same as saying (int)c + (int)d by the rules of the language.  And also within the expression context printf("%d\n", c); is the same as printf("%d\n", (int)c);

Even if you cast a char variable to char it will still immediately be promoted to int, so if you say (char)c that's the same as saying (int)(char)(int)c.  This is the reason that we can cast int i; to a shorter type (unsigned short)i and get a zero extended full sized int (from the lower 16 bits of i) as a result, or (short)i and get a sign extended full sized int (also from the lower 16 bits) as a result.

This automatic and immediate promotion to int for the shorter data types happens independently of function calling and parameter passing.  So, in printf("%d\n", c); we are passing an int (that happens to be widened from a char) and that's what printf sees.

but why char and short need the promotion?

This is by the definition of the language.  We can guess at rationale, namely that it simplifies the arithmetic operators, and also that we need some rules to rely upon even if they were different from that.


From ISO/IEC 9899:201x Committee Draft — April 12, 2011 N1570

EXAMPLE 2
In executing the fragment
char c1, c2;
/* ... */
c1 = c1 + c2;
the ‘‘integer promotions’’ require that the abstract machine promote the value of each variable to int size and then add the two ints and truncate the sum. Provided the addition of two chars can be done without §5.1.2.3 Environment 15 overflow, or with overflow wrapping silently to produce the correct result, the actual execution need only produce the same result, possibly omitting the promotions.

Erik Eidt
  • 23,049
  • 2
  • 29
  • 53
  • why `there is no char arithmetic in the language`, Do you have some documentation? – OnlyWick Jul 11 '22 at 19:19
  • The rules preclude char arithmetic at the level of the language, since `c + d` is defined as integer addition. However, that doesn't mean that a good compiler cannot use char arithmetic, as in `char c,d,e;` ... `e = c + d`, the compiler might well use char-sized add on x86 or x64, since there's no way within the language to observe whether it did or not. – Erik Eidt Jul 11 '22 at 19:22
  • I mean lower level, hardware! Is it hardware leads to something.., maybe about bitness? – OnlyWick Jul 11 '22 at 19:26
  • Ok, I provided a citation for the level of the language in addendum to my answer. – Erik Eidt Jul 11 '22 at 19:35
  • I'm not sure what you're getting at re: hardware, but for example, let's say that x86 and x64 both offer 8-bit add where as RISC V only offers 32-bit and 64-bit addition -- you can't even do 8-bit addition there. This is a simplification and works well with existing languages. If you actually wanted 8-bit addition, on RISC V you might work to detect overflow. Still, C doesn't let you see overflow, so while the 8-bit addition of x86 does set the flags properly, the C language doesn't let you see them. – Erik Eidt Jul 11 '22 at 19:35
  • The other thing is that while the x86 architecture may have byte- and short-width instructions (and a C compiler may or may not utilize them), I don't believe there are separate byte- or short-sized *registers*, are there? – Steve Summit Jul 11 '22 at 20:19
  • 1
    @SteveSummit, I think most people would refer to those byte and short sized registers as aliases for portions of the full sized register rather than being their own registers, but it is an implementation detail that we don't know from the instruction set itself -- an older processor may have had the exact 8 bit registers as per the ISA, that it combines two to make 16-bits when so needed, while a new processor may have hundreds of 64-bit registers shadowing the architectural registers of the instruction set. – Erik Eidt Jul 11 '22 at 20:46
  • Erik Eidt, "`(char)c` that's the same as saying `(int)(char)(int)c`" has a simple counter example: `char c; printf("%zu\n", sizeof ((char)c));` prints 1, not more. Perhaps you are referring to select code. – chux - Reinstate Monica Jul 11 '22 at 21:04
  • @chux-ReinstateMonica, yes, `sizeof` is very special, treating the expression as a type. `sizeof(i++)`, for example, is legal C code, but running that won't increment `i`. – Erik Eidt Jul 11 '22 at 21:05
  • @OnlyWick: Usually it is only a performance tuning choice, because the rules for casting back to a narrower type, plus the as-if rule, make it equivalent to do 8-bit math. Like `++my_char` with `inc %al` would be possible but usually sub-optimal. But this answer correctly explains why your example doesn't allow that freedom. – Peter Cordes Jul 11 '22 at 22:33
  • 1
    @ErikEidt: This answer is mostly right, except that x86 ABIs (unlike many RISCs) allow high garbage in args and return values: [Is garbage allowed in high bits of parameter and return value registers in x86-64 SysV ABI?](https://stackoverflow.com/q/40475902). (Unofficially, narrow args (not retvals) must be sign- or zero-extended to 32-bit; clang depends on this, GCC doesn't but does it anyway.) Clang aggressively uses partial registers even when it has no benefit and creates false dependencies (or partial reg stalls on old CPUs). Sometimes it *does* save instructions. – Peter Cordes Jul 11 '22 at 22:35
  • @PeterCordes, ok, thanks for the correction & clarification! – Erik Eidt Jul 11 '22 at 22:36
  • @PeterCordes ohh, Thanks, It's hard for me to understand these Optimizations! – OnlyWick Jul 12 '22 at 03:56