The essence of C language (3): structures and unions

Structures and Unions

Let's use the disassembly method to study the structure of the C language:

example:

      #include <stdio.h>
      int main(void)
      {
          struct {
                  char a;
                  short b;
                  int c;
                  char d;
          } s;
          s.a = 1;
          s.b = 2;
          s.c = 3;
          s.d = 4;
          printf("%u\n", sizeof(s));
          return 0;
      }

The disassembly results of several statements in the main function are as follows:

          s.a = 1;
      80483ed:   c6 44 24 14 01          movb   $0x1,0x14(%esp)
          s.b = 2;
      80483f2:   66 c7 44 24 16 02 00   movw   $0x2,0x16(%esp)
          s.c = 3;
      80483f9:   c7 44 24 18 03 00 00   movl   $0x3,0x18(%esp)
      8048400:   00
          s.d = 4;
      8048401:   c6 44 24 1c 04          movb   $0x4,0x1c(%esp)

As can be seen from the instructions for accessing the members of the structure, the arrangement of the four members of the structure on the stack is shown in Figure 18.5.

Although the stack grows from high address to low address , the structure members are also arranged from low address to high address , which is similar to an array.

But one thing is different from an array. The members of the structure are not arranged next to each other. There is a gap in the middle, which is called padding. Not only that, there are also three bytes of padding at the end of the structure, so The value of sizeof(s) is 12.

Why does the compiler do this? There is a point of knowledge that I have been avoiding before. Most computer architectures have restrictions on instructions that access memory. On a 32-bit platform, if an instruction accesses 4 bytes (such as movl above), the starting memory The address should be an integer multiple of 4. If an instruction accesses two bytes (such as movw above), the starting memory address should be an integer multiple of 2. This is called alignment (Alignment), and an instruction that accesses one byte (such as movb above) has no alignment requirements.

What happens if the memory address accessed by the instruction is not properly aligned? On some platforms, it will not be able to access the memory, causing an exception. On the x86 platform, it can access the memory, but the execution efficiency of unaligned instructions is lower than that of aligned instructions, so the compiler will take into account when arranging the addresses of various variables. Alignment issues.

For the structure in this example, the compiler will align its base address to a 4-byte boundary, that is, the address esp+0x14 must be an integer multiple of 4.

  • sa occupies one byte, there is no alignment problem.

  • sb occupies two bytes. If sb is immediately behind sa, its address cannot be an integer multiple of 2, so the compiler will insert a padding byte into the structure so that the address of sb is an integer multiple of 2. .

  • sc occupies 4 bytes, just behind sb, because the address esp+0x18 is also an integer multiple of 4.

Why should there be padding bits behind sd to the 4-byte boundary? This is for the convenience of arranging the variable addresses behind this structure. If an array is formed with this type of structure, then since the end of the previous structure has padding bytes aligned to the 4-byte boundary, the latter structure only It needs to be arranged next to the previous structure.

In fact, the C standard stipulates that array elements must be arranged next to each other without gaps , so as to ensure that the address of each element can be simply calculated according to "base address + n × number of bytes of each element".

Reasonable design of the arrangement order of the members of the structure can save storage space. If the structure in the above example is changed to this, the generation of padding bytes can be avoided:

      struct {
              char a;
              char d;
              short b;
              int c;
      } s;

In addition, gcc provides an extended syntax to eliminate padding bytes in structures:

      struct {
              char a;
              short b;
              int c;
              char d;
      } __attribute__((packed)) s;

But in this way, the alignment of structure members cannot be guaranteed, and there may be efficiency problems when accessing b and c, or even inaccessible, so unless there is a special reason, generally do not use this syntax.

The data types we talked about before occupy at least one byte, and the Bit-field syntax can also be used in the structure to define members that only occupy a few bits. The following example is from Wang Cong's website (http://www.wangcong.org/):

Bit-field

      #include <stdio.h>
      typedef struct {
              unsigned int one:1;
              unsigned int two:3;
              unsigned int three:10;
              unsigned int four:5;
              unsigned int :2;
              unsigned int five:8;
              unsigned int six:8;
      } demo_type;
      int main(void)
      {
              demo_type s = { 1, 5, 513, 17, 129, 0x81 };
              printf("sizeof demo_type = %u\n", sizeof(demo_type));
              printf("values: s=%u,%u,%u,%u,%u,%u\n",
                    s.one, s.two, s.three, s.four, s.five, s.six);
              return 0;
      }

The layout of the s structure is shown in Figure 18.6:

Bit-field is also an integer type, which can be declared with int or unsigned int, indicating signed or unsigned number, but it does not occupy 4 bytes like ordinary int type, and the number after the colon indicates that this Bit-field occupies A few bits. The unsigned int :2; in the above example defines an unnamed Bit-field occupying two bits. Even if the unnamed Bit-field is not written, the compiler may insert a padding bit between two members, for example, there is a padding bit between five and six in Figure 18.6, so that the six member just occupies a single byte. The access efficiency will be higher, and 3 bytes are padded at the end of this structure to align to a 4-byte boundary. We said before that the Byte Order of x86 is little-endian. From the arrangement order of one and two in Figure 18.6, we can see that if a byte is further subdivided, the Bit Order in the byte is also little-endian, because it is arranged in the structure Members at the front of the body (members near the lower address side) take the lower bits in the byte. How to arrange Bit-fields is not clearly stipulated in the C standard. This is related to issues such as Byte Order, Bit Order, and alignment. Different platforms and compilers may arrange them very differently. It is impossible to write portable code. It is assumed that Bit-fields are arranged in a certain fixed way. Bit-field is very useful in the driver, because it is often necessary to operate one or several bits in the device register separately, but it must be used carefully, first figure out the correspondence between each Bit-field and each bit in the device register relation.

In the above example, I did not give the disassembly result, but directly drew a picture saying that the layout of this structure is like this, so what evidence do I have to say so? The disassembly result of the above example is rather cumbersome, we can use another method to get the memory layout of this structure:

Consortium

      #include <stdio.h>
      typedef union {
          struct {
                  unsigned int one:1;
                  unsigned int two:3;
                  unsigned int three:10;
                  unsigned int four:5;
                  unsigned int :2;
                  unsigned int five:8;
                  unsigned int six:8;
              } bitfield;
              unsigned char byte[8];
      } demo_type;
      int main(void)
      {
              demo_type u = {
   
   { 1, 5, 513, 17, 129, 0x81 }};
              printf("sizeof demo_type = %u\n", sizeof(demo_type));
              printf("values: u=%u,%u,%u,%u,%u,%u\n",
                    u.bitfield.one, u.bitfield.two, u.bitfield.three,
                    u.bitfield.four, u.bitfield.five, u.bitfield.six);
              printf("hex dump of u: %x %x %x %x %x %x %x %x\n",
                    u.byte[0], u.byte[1], u.byte[2], u.byte[3],
                    u.byte[4], u.byte[5], u.byte[6], u.byte[7]);
              return 0;
      }

The keyword union defines a new data type, called a union, whose syntax is similar to a structure. Each member of a union occupies the same memory space, and the length of the union is equal to the length of the longest member. For example, the union of u occupies 8 bytes. If the member u.bitfield is accessed, the 8 bytes are regarded as a structure composed of Bit-fields. If the member u.byte is accessed, the 8 bytes are section as an array.

If the union is initialized with Initializer, only its first member will be initialized, for example, demo_type u ={ { 1, 5, 513, 17, 129, 0x81 }}; the initialization is u.bitfield, so we only know u. What is the value of each member of the bitfield structure, but we don’t know what its memory layout is like, and then we change the perspective, the same 8 bytes, we can see it as a u.byte array Find out how much each byte is and what the memory layout looks like.

If you use C99's Memberwise initialization syntax, you can initialize any member of the union, for example:

      demo_type u = { .byte = {0x1b, 0x60, 0x24, 0x10, 0x81, 0, 0, 0} };

Finally, review these concepts we have covered:

  • 1. The length of the data type (eg ILP32, LP64)
  • 2.Calling Convention
  • 3. Alignment requirements for accessing memory addresses
  • 4. Filling method of structure and Bit-field
  • 5. Byte order (big endian, little endian)
  • 6. What instructions are used to make system calls, and the parameters of various system calls
  • 7. Executable and library file formats (e.g. ELF format)

These are collectively referred to as the Application Binary Interface Specification (ABI, Application Binary Interface) . If the two platforms have the same architecture and follow the same ABI, it can be guaranteed that the binary program on one platform can be copied directly to another platform to run. , without recompiling. For example, if there are two x86 computers, one is a PC and the other is a netbook, and they are installed with different Linux distributions, then copying a binary program from one machine to another can also run, because the two The machines have the same architecture, and the operating systems follow the same ABI.

If two operating systems, Linux and Windows, are installed on the same computer, it is not acceptable to run a Linux binary program on the Windows system, because the ABIs of the two operating systems are different.

C inline assembly

Writing programs in C is more concise and readable than directly writing programs in assembly, but the efficiency may not be as good as assembly programs, because C programs need to generate assembly code through compilers after all, although modern compilers have done a good job of optimization , but still not as good as hand-written assembly code.

In addition, some platform-related instructions must be written by hand. There is no equivalent syntax in C language, because the concept in C language is an abstraction of various platforms, and some things specific to each platform will not appear in C language. , For example, x86 is port I/O, but C language does not have this concept, so in/out instructions must be written in assembly.

The C language is concise and easy to read, easy to organize large-scale codes, and has high assembly efficiency, and some special instructions must be written in assembly. In order to take advantage of these two aspects, gcc provides an extended syntax that can be used in C code Use inline assembly (Inline Assembly).

The simplest format is __asm__("assembly code");,

For example, __asm__("nop");, the nop instruction does nothing, just let the CPU idle for one instruction execution cycle.

If multiple assembly instructions need to be executed, each instruction should be separated by \n\t, for example:

      __asm__("movl $1, %eax\n\t"
              "movl $4, %ebx\n\t"
              "int $0x80");

Usually inline assembly needs to be associated with variables in C code, and the complete inline assembly format is used:

      __asm__(assembler template
              : output operands                /* optional */
              : input operands                 /* optional */
              : list of clobbered registers   /* optional */
              );

This format consists of four parts,

  • The first part is the assembly instruction, same as the above example,
  • The second and third parts are constraints. The second part tells the compiler which C language operands the operation results of the assembly instructions are to be output to. These operands should be lvalue expressions.
  • The third part tells the compiler which C language operands the assembly instruction needs to get input from,
  • The fourth part is the register list (called Clobber List) that is modified in the assembly instruction, which tells the compiler which register values ​​​​will change when executing this __asm__ statement.

The last three parts are optional, if there is, please fill in, if not, just write a colon, for example:

      #include <stdio.h>
      int main(void)
      {
            int a = 10, b;
              __asm__("movl %1, %%eax\n\t"
                    "movl %%eax, %0\n\t"
                    :"=r"(b)       /* output */
                    :"r"(a)        /* input */
                    :"%eax"        /* clobbered register */
                    );
              printf("Result: %d, %d\n", a, b);
              return 0;
      }

This program assigns the value of variable a to b. "r" (a) tells the compiler to allocate a register to save the value of variable a as the input of the assembly instruction, that is, %1 in the instruction (according to the order of constraints, b corresponds to %0, a corresponds to 1%), as It is up to the compiler to decide which register %1 represents.

The assembly instruction first passes the value of the register represented by %1 to eax (in order to distinguish it from the placeholder %1, two % signs are required in front of eax), and then passes the value of eax to the register represented by %0 register. "=r" (b) means to output the value of the register represented by %0 to variable b.

During the execution of these two instructions, the value of the register eax is changed, so write "%eax" in the fourth part, telling the compiler that eax will be rewritten when this __asm__ statement is executed, so here Do not use eax to save other values ​​during the period.

Let's take a look at the disassembly results of this program:

          __asm__("movl %1, %%eax\n\t"
      80483f5:   8b 54 24 1c         mov   0x1c(%esp),%edx
      80483f9:   89 d0               mov   %edx,%eax
      80483fb:   89 c2               mov   %eax,%edx
      80483fd:   89 54 24 18         mov   %edx,0x18(%esp)
                  "movl %%eax, %0\n\t"
                  :"=r"(b)       /* output */
                  :"r"(a)        /* input */
                  :"%eax"        /* clobbered register */
                  );

It can be seen that both %0 and %1 represent the edx register, first pass the value of the variable a (at the position of esp+0x1c) to edx and then execute the two instructions of inline assembly, and then pass the value of edx to b (at the position of esp+ 0x18 position).

References

"One-stop learning C programming"

Guess you like

Origin blog.csdn.net/weixin_45264425/article/details/132324809
Recommended