Do you know these advanced uses of embedded C?
[Copy link]
Memory ManagementWe need to know that variables are just abstract names of memory addresses. In statically compiled programs, all variable names will be converted into memory addresses during compilation. The machine does not know the names we give, but only the addresses. Memory usage is one of the important factors to be considered in program design. This is not only because the system memory is limited (especially in embedded systems), but also because memory allocation will directly affect the efficiency of the program. Therefore, we need to have a systematic understanding of memory management in C language. In C language, four memory areas are defined: code area; global variable and static variable area; local variable area, namely stack area; dynamic storage area, namely heap area; as follows: 1. Stack area (stack) - automatically allocated and released by the compiler, storing function parameter values, local variable values, etc. Its operation is similar to the stack in the data structure. 2. Heap area - Generally allocated and released by the programmer. If the programmer does not release it, it may be recovered by the OS when the program ends. Note that it is different from the heap in the data structure, and the allocation method is similar to a linked list. 3. Global area (static area) - The storage of global variables and static variables is placed together. Initialized global variables and static variables are in one area, and uninitialized global variables and uninitialized static variables are in another adjacent area. - Released by the system after the program ends. 4. Constant area - Constant strings are placed here. Released by the system after the program ends. 5. Program code area - Stores the binary code of the function body. Let's look at a picture: 407013 Figure 1 First of all, we must know that when the source code is compiled into a program, the program is placed on the hard disk, not in the memory! It will only be called into memory when it is executed! Let's take a look at the program structure. ELF is the main executable file format of Linux. ELF files are composed of 4 parts, namely ELF header, program header table, section and section header table. The details are as follows: 1. Program header describes the position and size of a segment in the file and its position and size after it is put into memory. That is, the information to be loaded; 2. Sections saves the information of the object file. From the connection point of view: including instructions, data, symbol table, relocation information, etc. In the figure, we can see that Sections include: text text node to store instructions; rodata data node readonly; data data node readable and writable; 3. The section header table contains information describing the file sections. Each section has an entry in this table; each entry gives the name, size, and other information of the section. It is equivalent to an index! How is the program distributed when it is loaded into memory? Let's look at the picture above: 1. The text, initialized data and uninitialized data are what we call the data segment, and the text is the code segment; 2. Above the text segment is the constant area, and above the constant area are the global variables and static variables area, which occupy the initialized data and uninitialized data; 3. Above it is the heap, the dynamic storage area, which is growing upward; 4. Above the heap is the stack, which stores local variables. After the code block where the local variables are located is executed, this memory will be released, and the stack area here is growing downward; 5. The command line parameters are 001 and the like. The environment variables have been discussed in the previous article. If you are interested, you can go and have a look. We know that memory is divided into dynamic memory and static memory. Let's talk about static memory first. Static Memory The storage model determines the memory allocation method and access characteristics of a variable. In C language, there are three main dimensions to determine it: storage period, scope, and linkage. Storage Period Storage period: the retention time (life cycle) of a variable in memory. The storage period is divided into two situations. The key is to see whether the variable will be automatically recycled by the system during program execution. 1) Static storage period Static will not be automatically recycled once allocated during program execution. Generally speaking, any variable that is not defined in a function-level code block. Whether it is in a code block or not, as long as the variable is modified with the static keyword. 2) Automatic storage period Automatic All variables except static storage are in automatic storage period, or in other words, as long as non-static variables are defined in a code block, the system will automatically allocate and release memory; Scope Scope: The visibility (access or reference) of a variable in the file where the variable is defined. In C language, there are 3 scopes: 1) Code block scope Variables defined in a code block have the scope of the code. From the place where the variable is defined to the end of the code block, the variable is visible; 2) Function prototype scope Variables that appear in a function prototype have function prototype scope, which runs from the variable definition to the end of the prototype declaration. 3) File scope A variable defined outside of any function has file scope. A variable with file scope is visible from its definition to the end of the file containing the definition; Link Link: the visibility (access or reference) of a variable in all files that make up a program; There are three different types of linkage in C language: 1) External linkage If a variable can be accessed anywhere in all files that make up a program, the variable is said to support external linkage; 2) Internal linkage If a variable can only be accessed anywhere in the file in which it is defined, the variable is said to support internal linkage. 3) Empty linkageIf a variable is private only to the current code block in which it is defined and cannot be accessed by other parts of the program, then the variable supports empty links. Let's take a look at a code example: #include int a = 0; // Global initialization area char *p1; //Global uninitialized area int main() { int b; //b is in the stack area char s[] = "abc"; //Stack char *p2; //p2 is in the stack area char *p3 = "123456"; //123456 is in the constant area, p3 is on the stack. static int c =0; //Global (static) initialization area p1 = (char *)malloc(10); p2 = (char *)malloc(20); //The allocated 10 and 20 byte areas are in the heap area. strcpy(p1, "123456"); //123456 is placed in the constant area, and the compiler may optimize it and the "123456" pointed to by p3 into one place. } 1.2 Dynamic Memory When the program runs to the point where a dynamically allocated variable is needed, it must apply to the system for a storage space of the required size in the heap to store the variable. When the variable is no longer in use, that is, when its life ends, the storage space it occupies must be released explicitly so that the system can reallocate the space again and reuse wired resources. The following are functions for dynamic memory allocation and release. 1.2.1 malloc function malloc function prototype: [attach]407015 [/attach] size is the number of bytes of memory that needs to be dynamically allocated. If the allocation is successful, the function returns the starting address of the allocated memory. If the allocation fails, it returns NULL. Let's look at the following example: [attach]407016 [/attach] When using this function, there are a few points to note: 1) Only care about the size of the allocated memory; 2) A continuous block of memory is allocated. Remember to write an error check; 3) Display initialization. That is, we don't know what is in this memory, so we need to clear it to zero; 1.2.2 free function The memory allocated on the heap needs to be released explicitly using the free function. The function prototype is as follows: [attach]407017 [/attach] When using free(), there are also the following points to note: 1) The starting address of the memory must be provided; When calling this function, the starting address of the memory must be provided, and a partial address cannot be provided. Releasing a part of the memory is not allowed. 2) malloc and free are used in pairs; The compiler is not responsible for the release of dynamic memory, and the programmer needs to release it explicitly. Therefore, malloc and free are used in pairs to avoid memory leaks. p = NULL is necessary, because although this memory has been released, p still points to this memory to avoid the next misoperation of p; 3) Repeated release is not allowed because after this memory is released, it may have been allocated again. If this area is occupied by others, if it is released again, it will cause data loss; 1.2.3 Other related functions The calloc function needs to consider the type of storage location when allocating memory. The realloc function can adjust the size of a dynamically allocated memory 1.3 Comparison between heap and stack 1) Application method stack: Automatically allocated by the system. For example, declare a local variable int b in a function; the system automatically allocates space for b in the stack. Heap: The programmer needs to apply for it himself and specify the size. In C, the malloc function is like p1 = (char *)malloc(10); 2) The system's response stack after application: As long as the remaining space of the stack is larger than the space applied for, the system will provide memory for the program, otherwise it will report an exception to indicate stack overflow. Heap: First of all, you should know that the operating system has a linked list that records free memory addresses. When the system receives an application from a program, it will traverse the linked list to find the first heap node with a space larger than the space applied for, then delete the node from the free node linked list and allocate the node's space to the program. In addition, for most systems, the size of this allocation will be recorded at the first address in this memory space, so that the delete statement in the code can correctly release the memory space. In addition, since the size of the found heap node is not necessarily exactly equal to the size applied for, the system will automatically put the excess part back into the free linked list. 3) Limitation of application size Stack: The stack is a data structure that expands to a lower address and is a continuous memory area. This means that the address of the top of the stack and the maximum capacity of the stack are pre-determined by the system. The size of the stack is 2M (some say 1M, anyway, it is a constant determined at compile time). If the space applied for exceeds the remaining space of the stack, an overflow will be prompted. Therefore, the space that can be obtained from the stack is small. Heap: The heap is a data structure that expands to a higher address and is a discontinuous memory area. This is because the system uses a linked list to store free memory addresses, which is naturally discontinuous, and the traversal direction of the linked list is from low address to high address. The size of the heap is limited by the effective virtual memory in the computer system. It can be seen that the space obtained by the heap is more flexible and larger. 4) Comparison of application efficiency The stack is automatically allocated by the system and is faster. But the programmer cannot control it. The heap is the memory allocated by new, which is generally slower and prone to memory fragmentation,But it is the most convenient to use. 5) Storage contents in the heap and stackStack: When a function is called, the first thing pushed into the stack is the address of the next instruction after the main function (the next executable statement after the function call statement), followed by the various parameters of the function. In most C compilers, the parameters are pushed into the stack from right to left, followed by the local variables in the function. Note that static variables are not pushed into the stack. When the current function call ends, the local variables are popped out of the stack first, followed by the parameters, and finally the top pointer of the stack points to the address stored at the beginning, which is the next instruction in the main function, and the program continues to run from this point. Heap: Generally, one byte is used at the head of the heap to store the size of the heap. The specific content of the heap is arranged by the programmer. 6) Comparison of access efficiency char s1[] = "aaaaaaaaaaaaaaaaa"; char *s2 = "bbbbbbbbbbbbbbbbb"; aaaaaaaaaaaaa is assigned at runtime, while bbbbbbbbbbbb is determined at compile time. However, in subsequent accesses, the array on the stack is faster than the string pointed to by the pointer (such as the heap). For example: The corresponding assembly code The first type directly reads the elements in the string into register cl when reading, while the second type first reads the pointer value into edx, and then reads the character according to edx, which is obviously slower. 7) Finally, the difference between the heap and the stack can be seen by the following metaphor: The stack is like when we go to a restaurant to eat, we only need to order dishes (issue an application), pay, and eat (use), and leave when we are full. We don’t have to worry about the preparation work such as cutting vegetables and washing vegetables, and the finishing work such as washing dishes and washing pots. Its advantage is that it is fast, but the degree of freedom is small. The stack is like making your favorite dishes by yourself, which is more troublesome, but more in line with your taste, and has a large degree of freedom. 2. Memory alignment 2.1 #pragma pack(n) Alignment usage details 1. What is alignment and why do we need alignment? In modern computers, memory space is divided into bytes. In theory, it seems that any type of variable can be accessed from any address, but in reality, when accessing a specific variable, it is often accessed at a specific memory address. This requires that data of various types be arranged in space according to certain rules, rather than arranged one by one in sequence. This is alignment. The role and reason of alignment: Each hardware platform has a very different way of handling storage space. Some platforms can only access certain types of data from certain addresses. Other platforms may not have this situation, but the most common thing is that if the data storage is not aligned according to the requirements of the platform, it will cause a loss in access efficiency. For example, some platforms start reading from an even address each time. If an int type (assuming a 32-bit system) is stored at the beginning of an even address, it can be read out in one read cycle. If it is stored at the beginning of an odd address, it may take two read cycles, and the high and low bytes of the results of the two reads must be pieced together to get the int data. Obviously, the reading efficiency is greatly reduced. This is also a game of space and time. 2. Alignment implementation Usually, when we write a program, we don't need to consider alignment issues. The compiler will choose the alignment strategy of the target platform for us. Of course, we can also notify the compiler to pass pre-compilation instructions to change the alignment method for the specified data. However, precisely because we generally don't need to care about this issue, because the editor has aligned the data storage, and we don't understand it, we often get confused about some problems. The most common is the sizeof result of the struct data structure, which is unexpected. For this reason, we need to understand the alignment algorithm. Function: Specify the packing alignment of structures, unions, and class members; Syntax: #pragma pack( [show] | [push | pop] [, identifier], n ) Description: 1>pack provides control at the data declaration level and has no effect on definitions; 2>When calling pack without specifying a parameter, n will be set to the default value; 3>Once the alignment of the data type is changed, the direct effect is a reduction in memory usage, but performance will decrease; 3. Specific syntax analysis 1>show: optional parameter; displays the number of bytes of the current packing alignment, which is displayed in the form of a warning message; 2>push: optional parameter; pushes the currently specified packing alignment value onto the stack, where the stack is the internal compiler stack, and sets the current packing alignment to n; if n is not specified, pushes the current packing alignment value onto the stack; 3>pop: optional parameter; pushes the current packing alignment value onto the stack from the internal compiler stack; Delete the top record in the compiler stack; if n is not specified, the current top record is the new packing alignment value; if n is specified, n will become the new packing alignment value; if identifier is specified, all records in the internal compiler stack will be popped until the identifier is found, and then the identitier will be popped out, and the packing alignment value will be set to the record at the top of the current stack; if the specified identifier does not exist in the internal compiler stack, the pop operation will be ignored; 4>identifier: optional parameter; when used with push, a name is given to the record currently pushed into the stack; when used with pop, all records are popped out from the internal compiler stack until the identifier is popped out, and if the identifier is not found, the pop operation is ignored; 5>n: optional parameter; specifies the packing value in bytes; the default value is 8, and the legal values are 1, 2, 4, 8, and 16. 4.Important rules 1> The members of a complex type are stored sequentially in memory in the order in which they are declared, and the address of the first member is the same as the address of the entire type; 2> Each member is aligned separately, that is, each member is aligned in its own way and minimizes its length; the rule is that each member is aligned according to the smaller of the alignment parameter of its type (usually the size of the type) and the specified alignment parameter; 3> The first data member of a structure, union or class is placed at offset 0; the alignment of each subsequent data member is based on the smaller of the value specified by #pragma pack and the length of the data member itself; that is, when the value specified by #pragma pack is equal to or exceeds the length of all data members, the size of the specified value will have no effect; 4> The overall alignment of a complex type (such as a structure) is based on the smaller value between the data member with the largest length in the structure and the value specified by #pragma pack; this way, when the member is a complex type, the length can be minimized; 5>The calculation of the overall length of the structure must be an integer multiple of all the alignment parameters used, and the missing bytes are filled; that is, the integer multiple of the largest value of all the alignment parameters used, because the alignment parameters are all 2 to the power of n; this way, when processing arrays, each item can be guaranteed to be aligned to the boundary; 5. Due to the differences in the algorithms of each platform and compiler, I will now use the gcc version 3.2.2 compiler (32-bit x86 platform) I use as an example to discuss how the compiler aligns the members of the struct data structure. Under the same alignment method, the order of data definition inside the structure is different, and the overall memory space occupied by the structure is also different, as follows: Assume that the structure is defined as follows: Structure A contains a 4-byte int, a 1-byte char, and a 2-byte short data. So the space used by A should be 7 bytes. But because the compiler needs to align the data members in space. So the value of sizeof(strcut A) is 8. Now adjust the order of the member variables of the structure. At this time, there are still 7 bytes of variables in total, but the value of sizeof(struct B) is 12. Next, we use the precompilation directive #progma pack (value) to tell the compiler to use the alignment value we specify instead of the default. The value of sizeof(struct C) is 8. Change the alignment value to 1: The value of sizeof(struct D) is 7. For char data, its own alignment value is 1, for short data, it is 2, for int, float, double data, its own alignment value is 4, in bytes. 6. Four concept values 1> The alignment value of the data type itself: it is the alignment value of the basic data type explained above. 2> Specified alignment value: the specified alignment value value when #progma pack (value). 3> The alignment value of the structure or class itself: the value with the largest alignment value among its data members. 4> The effective alignment value of data members, structures and classes: the smaller value between the alignment value and the specified alignment value. With these values, we can easily discuss the members of a specific data structure and its own alignment. The effective alignment value N is the most important value used to determine the data storage address. Effective alignment N means "aligned on N", that is, the "storage start address %N=0" of the data. The data variables in the data structure are arranged in the order of definition. The start address of the first data variable is the start address of the data structure. The member variables of the structure must be aligned, and the structure itself must be rounded according to its own effective alignment value (that is, the total length occupied by the member variables of the structure must be an integer multiple of the effective alignment value of the structure, combined with the following examples to understand). In this way, the values of the above examples cannot be understood. Example analysis: Analysis of example B; Assume that B starts at address space 0x0000. No specified alignment value is defined in this example. In the author's environment, the value defaults to 4. The first member variable b has an alignment value of 1, which is smaller than the specified or default alignment value of 4, so its effective alignment value is 1, so its storage address 0x0000 complies with 0x0000%1=0. The second member variable a has an alignment value of 4, so its effective alignment value is also 4, so it can only be stored in the four consecutive byte spaces from 0x0004 to 0x0007, which complies with 0x0004%4=0, and is close to the first variable. The third variable c has an alignment value of 2, so its effective alignment value is also 2, and it can be stored in the two byte spaces from 0x0008 to 0x0009, which complies with 0x0008%2=0. So the contents of B are stored from 0x0000 to 0x0009. The alignment value of data structure B is the largest alignment value of its variables (here is b), so it is 4, so the effective alignment value of the structure is also 4. According to the rounding requirement of the structure, 0x0009 to 0x0000 = 10 bytes, (10 + 2) % 4 = 0. Therefore, 0x0000A to 0x000B is also occupied by structure B. Therefore, B has a total of 12 bytes from 0x0000 to 0x000B, sizeof(struct B)=12; Similarly, analyze the above example C: The first variable b has its own alignment value of 1, and the specified alignment value is 2, so its effective alignment value is 1. Assuming that C starts at 0x0000, then b is stored at 0x0000, which complies with 0x0000%1=0; The second variable has its own alignment value of 4, and the specified alignment value is 2, so the effective alignment value is 2, so it is stored in four consecutive bytes at 0x0002, 0x0003, 0x0004, and 0x0005, which complies with 0x0002%2=0. The third variable c has an alignment value of 2, so the effective alignment value is 2, and is stored in 0x0006 and 0x0007 in sequence, which conforms to 0x0006%2=0. Therefore, the eight bytes from 0x0000 to 0x00007 store the variable C. And the alignment value of C is 4, so the effective alignment value of C is 2. And 8%2=0,C only occupies eight bytes from 0x0000 to 0x0007. So sizeof(struct C)=8. 9.2.2 The impact of byte alignment on the program Let's take a look at a few examples first (32bit, x86 environment, gcc compiler): Suppose the structure is defined as follows: It is now known that the lengths of various data types on 32-bit machines are as follows: char: 1 (signed and unsigned are the same) short: 2 (signed and unsigned are the same) int: 4 (signed and unsigned are the same) long: 4 (signed and unsigned are the same) float: 4 double: 8 So what are the sizes of the above two structures? The result is: sizeof(strcut A) is 8, but the value of sizeof(struct B) is 12 Structure A contains a 4-byte int, a 1-byte char and a 2-byte short, and the same is true for B; it stands to reason that the sizes of A and B should both be 7 bytes. The above result is because the compiler needs to align the data members in space. The above is the result of alignment according to the default settings of the compiler. So can we change the default alignment settings of the compiler? Of course we can. For example: The value of sizeof(struct C) is 8. Change the alignment value to 1: The value of sizeof(struct D) is 7. We will explain the role of #pragma pack() later. 2.3 Modify the default alignment value of the compiler 1> In VC IDE, you can modify it like this: [Project]|[Settings], C/C++ tab Category Code Generation option Struct Member Alignment, the default is 8 bytes. 2> When coding, you can modify it dynamically like this: #pragma pack. Note: it is pragma, not progma. If we want to consider saving space when programming, then we only need to assume that the first address of the structure is 0, and then arrange the variables according to the above principles. The basic principle is to declare the variables in the structure from small to large according to the type size, and try to reduce the filling space in the middle. Another way is to exchange space for time efficiency. We explicitly fill the space for alignment. For example, one way to use space for time is to explicitly insert the reserved member: The reserved member has no meaning for our program. It only fills the space to achieve the purpose of byte alignment. Of course, even if this member is not added, the compiler will usually automatically fill the alignment for us. We add it ourselves just to serve as an explicit reminder. 2.4 Potential hidden dangers of byte alignment Many of the hidden dangers of alignment in the code are implicit. For example, when forcing type conversion. For example: [attach]407031 [/attach] The last two lines of code, accessing unsignedshort variables from odd boundaries, obviously do not comply with the alignment requirements. On x86, similar operations will only affect efficiency, but on MIPS or sparc, it may be an error because they require byte alignment. If alignment or assignment problems occur, first check 1). The big little end settings of the compiler 2). See if this system itself supports unaligned access 3). If it supports it, see if alignment is set or not. If not, see if some special modifiers are needed during access to mark its special access operation. Alignment processing under ARM from DUI0067D_ADS1_2_CompLib type qulifiers Some of them are excerpted from the ARM compiler document alignment part alignment usage: 1.__align(num) This is used to modify the byte boundary of the highest level object. When using LDRD or STRD in assembly, the command __align(8) is used to modify and limit. To ensure that the data object is aligned accordingly. This modification command has a maximum limit of 8 bytes. It can make a 2-byte object 4-byte aligned, but it cannot make a 4-byte object 2-byte aligned. __align is a storage class modification. It only modifies the highest-level type object and cannot be used for structure or function objects. 2.__packed __packed is a one-byte alignment. l Cannot align packed objects. l All read and write accesses of objects are unaligned. l Float and structure unions containing float and objects that are not __packed will not be byte aligned. l __packed has no effect on local integer variables. l Forced conversion from unpacked objects to packed objects is undefined. Integer pointers can be legally defined as packed. __packed int* p; //__packed int is meaningless 2.5 Aligned or unaligned read and write accesses bring problems __packed struct STRUCT_TEST {char a;int b;char c; } ; //Define the following structure. At this time, the starting address of b must be unaligned. There may be problems accessing b on the stack, because the data on the stack must be aligned for access[from CL] //Define the following variables as global static and not on the stack static char* p;static struct STRUCT_TEST a;void Main() { __packed int* q; //At this time, define it as __packed to modify the current q pointing to the unaligned data address. The following access is OK p = (char*)&a; q = (int*)(p+1); *q = 0x87654321; /* The assembly instruction to get the value is very clear ldr r5,0x20001590 ; = #0x12345678 [0xe1a00005] mov r0,r5 [0xeb0000b0] bl __rt_uwrite4 //Call a 4-byte write function here [0xe5c10000] strb r0,[r1,#0] //The function performs 4 strb operations and then returns to ensure correct data access [0xe1a02420] mov r2,r0,lsr #8 [0xe5c12001] strb r2,[r1,#1] [0xe1a02820] mov r2,r0,lsr #16 [0xe5c12002] strb r2,[r1,#2] [0xe1a02c20] mov r2,r0,lsr #24 [0xe5c12003] strb r2,[r1,#3] [0xe1a0f00e] mov pc,r14 */ /* If q is not modified with __packed, the instruction will be assembled like this, which will directly cause access failure at odd addresses [0xe59f2018] ldr r2,0x20001594 ; = #0x87654321 [0xe5812000] str r2,[r1,#0] */ //This way you can clearly see how unaligned accesses cause errors //And how to eliminate the problems caused by unaligned accesses //You can also see that the difference in instructions between unaligned accesses and aligned accesses leads to efficiency problems } Source: Network compilation. If copyright is involved, please contact us to delete.
|