How C/C++ runs and its distribution in memory

C/C++ Restart? 1.0

Question: How does the written C++ code run? In the following order from left to right
Writing source code Preprocessing Compilation Assembly Linking Loading Execute()
  • Writing source code: writing in high-level programming languages, such as following C/C++ syntax rules to write good code correctly
  • Preprocessing: Before compilation, the preprocessor processes header files and macro expansion to generate intermediate code called “translation units”
  • Compilation: The preprocessed code is sent to the compiler. The compiler translates source code into assembly code.
  • Assembly: The assembler translates the assembly code into machine code or object code to generate an executable file.
  • Linking: If a program contains multiple source files, the compiler generates multiple object files, and the linker combines these object files and possibly library files into an executable file.
  • Loading: The operating system loads the executable file into memory and hands control to the program’s entry point, usually the main function.
  • Execution: Perform corresponding operations according to the logic defined in the source code.

Preprocessing

The work of the preprocessor is controlled by preprocessor directives (directives starting with #), which modify the source code before compilation.

#include <iostream> //Used to include the header file and insert the specified file content into the current file.
#define PI 3.14159 //Used to define macros, replace all PIs with 3.13159 in the preprocessing stage
#define SQUARE(x) ((x) * (x)) //Example of macro expansion and replacement
int num = 5;
//Before preprocessing
int result = SQUARE(num);
//After preprocessing
int result = ((num) * (num));
#define _TEST_//Define macro
// ...
#undef _TEST_//Cancel macro
//#ifdef, #endif: used for conditional compilation, including or excluding code blocks based on whether the specified macro is defined.
#ifdef _TEST_
    //If the macro _TEST_ is defined, this part is retained. If it is not defined, this part is discarded.
#endif
//#if, #else, #endif: used for conditional compilation, including or excluding code blocks based on specified conditions. Same as above
#if defined(_TEST_)
    //
#else
    //
#endif
#pragma once//Provides a way to issue specific commands to the compiler, usually used to control the behavior of the compiler.
//#error: Used to generate compilation errors during the preprocessing phase, which can be used to force programmer attention or interrupt compilation under specific conditions.
#ifdef LINUX
    //Linux-specific code
#else
    #error "Unsupported platform"
#endif

TODO: #pragma once additional explanation example

Compile

  • Lexical analyzer: Also called a scanner, it inputs the source program, performs lexical analysis, and outputs word symbols.

  • Grammar analyzer, also known as analyzer, performs grammatical analysis on word symbol strings, identifies various grammatical units, and finally determines whether the input string constitutes a grammatically correct “program”.

  • Semantic analysis and intermediate code generator: Perform semantic analysis on the grammatical units reduced (or derived) by the syntax analyzer according to semantic rules, and translate them into a certain form of intermediate code.

  • Optimizer: Optimize the intermediate code.

  • Object code generator: Translates intermediate code into object code.

Table management: used to register various information of the source program and the progress of each stage of compilation.
The role of error handling: discover various errors to the greatest extent possible. Pinpoint the nature of the error and where it occurred. Limit the impact of the error to the smallest possible scope so that the rest of the source program can continue to be compiled to further discover other possible errors. Correct errors automatically if possible.

Question: What is intermediate code and what is target code?

Intermediate code: Intermediate code is an abstract representation between source code and target code. During the compilation process, the source code is first converted into intermediate code, which is usually easier to optimize and port across platforms. What if we do not generate intermediate code but directly generate target code in the form of machine language or assembly language? The advantage is that the compilation time is short, but the disadvantage is that the target code execution efficiency and quality are relatively low, and the portability is poor.

The expression is as follows (easy to understand)

  • Suffix form (reverse Polish)
  • Graphical representation
  • Three address codes

Object code: The intermediate code of the source program is used as input, and an equivalent target program is produced as output; the target code has three possible forms

  • Machine language code that can be executed immediately, with all addresses located.
  • Machine language module to be assembled.
  • Assembly language code must be subsequently compiled by an assembler and converted into executable machine language code.

[Assembly code files are usually .asm files in the Windows environment and .s files in the Linux environment]

IDE (Integrated Development Environment): IDE is a software that integrates tools such as editor, compiler and debugger.

C/C++ common compilers
  • GCC (GNU Compiler Collection): An open source compiler collection that supports multiple programming languages, including C and C++. Used by default on many Linux and Unix systems.
  • Clang: A compiler developed by the LLVM project that supports C, C++ and other languages. Clang has fast compilation speed and advanced diagnostic capabilities.
  • MSVC (Microsoft Visual C++ Compiler): Microsoft Visual Studio integrated compiler for compiling C++ code on Windows platforms.
C/C++ common debuggers
  • GDB (GNU Debugger): A powerful open source debugger for many programming languages, including C and C++. Integrated with GCC, commonly used in Linux systems.
  • LLDB: A debugger developed by the LLVM project that provides functionality similar to GDB but with a more modern design. Commonly used on macOS systems.
  • Visual Studio Debugger: Microsoft Visual Studio integrated debugger, suitable for developing and debugging C++ code on Windows platforms.
  • Xcode Debugger: Xcode is an integrated development environment provided by Apple for developing macOS and iOS applications. It includes a debugger that can be used to debug Objective-C and C++ code.

Compilation

The assembler converts the assembly code into machine code machine language and generates the target file .o file under Linux (.obj file under Window environment). can be called target module

TODO: You need to understand assembly code to learn

Link

The linker linker links the assembled set of target modules and the required library functions together to form a complete load module (the load module is also the *.exe file)

Three ways of linking

  1. Static linking: Before running, link all target modules and included library functions into a complete load module (executable file) and then do not disassemble it. Confirm the complete logical address,
  2. Dynamic linking when loading: When loading, when each target module is loaded into the memory, it is linked while loading.
  3. Runtime dynamic linking: Runtime, the required target modules are linked to it. The advantage is that it is easy to modify and update, and it is easy to realize the sharing of target modules.

Load

The loader program loads the load module into memory and runs it.

Basic principles of process operation

  • How the command works

eg: Code x=x + 2; will be converted into an instruction set after compilation. Assume that the current compiled instruction set has a total of 3 instructions (machine instructions)

They are operation code (what operation to do), address code (where to get the operand), and data (data of the operation)

That is, as follows, the CPU knows through the operation code 00010010 that it wants to execute the variable x with the address code 01010011, plus the data 00000010, and then returns to the address code 01010011. 【Hypothesis】

Operation code 00010010 Address code 01010011 Data 00000010

The process consists of three parts in the memory, the program segment, the data segment, and the PCB. The instruction exists in the program segment, and the x data is in the data segment. Internally maintained, instructions are executed to the CPU in order from top to bottom according to the program counter. But the instruction addresses of all these are relative to the process itself. That is to say, it is understood that the logical addresses of 0000 0000 ~ 1111 1111 are known by the memory itself. But I don’t know if I put it into memory. Assume that the memory is 4G = 232B;

That is, 0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111 are all the actual physical addresses of the memory

If the physical address of 0000 0000 0000 ~ 0000 1111 1111 is placed at this time, the addresses of the address code and opcode compiled by the local instructions will be wrong, because the reference object is 0000 0000 ~ 1111 1111.

attention: The process starting address is set to 0, but the address placed in the memory does not necessarily start from 0.

  • Logical address and physical address

Logical address (relative address): We will all set the address of a process to start from 0, and the logical address is the address relative to the actual address of the process. (You can understand the offset address in the process)

Physical address (absolute address): an address relative to memory.

How to implement address translation

How to convert the logical (relative) address in the instruction to the physical (absolute) address? The strategy is three loading methods

  • Absolute loading: When compiling, knowing in advance where it will be placed in the memory, the compiler can generate the target code with an absolute address, and then link it to the load module. The loader will also follow the instructions of the load module. The address of the target code is correct. (When compiling the program)
    • Because the compiler knows where to put the memory, what I can write on my computer may not work on someone else’s computer. Low flexibility, only suitable for single-programming environment
  • Relocatable loading (static relocation): Compilation, linking to the loaded module or logical address, when loaded into memory, the address is “relocated” that is, the logical address is converted into a physical address. Loading is done in one go. (When the loader loads memory)
    • Since this is a one-time loading, all space must be allocated when the job is loaded into the memory. If it is not enough, the job cannot be loaded. Once the job is loaded into the memory, it cannot be moved during operation, nor can it apply for new space. Because the physical address is written to death
  • Dynamic run loading (dynamic relocation): (Modern operating systems) compile, link to the load module, and are all logical addresses after loading into memory. Address translation is performed only when the program is actually executed. Requires a relocation register support. (Runtime)
    • The base address/relocation register stores the starting address of the loaded module into the memory (understood as recording the absolute address of the program). That is, the program wants to execute the instruction at logical address 79. The relocation register is 100, that is, the program starts from memory 100. It is stored, that is, the instruction is at location 179 in the memory.
    • The running program moves in the memory, and can also allocate discontinuous storage areas. You can load part of the code first, dynamically apply for memory and put it in as needed [relevant to virtual storage management]
Question: Memory partition during C/C++ runtime

The execution process of a C/C++ program placed in memory can be understood as a process. The process is divided into PCB, data segment and data segment. The relationship with the four memory areas is as follows

Code snippet:

  • code area

Data segment:

  • global static area
    • bss section: global uninitialized, static uninitialized data
    • data section: global initialization, static initialization data
    • Constant section: constant data
  • Heap area
  • stack area

File mapping area: It is different from the traditional division of code segments and data segments. It provides a mechanism to associate files in the virtual address space. This mapping is usually implemented through functions such as mmap (on Unix-like systems) or MapViewOfFile (on Windows) provided by the operating system. The file mapped area is usually located in the virtual address space of the process, allowing the process to directly access the file contents without explicitly calling read or write functions.

High address 0xFFFF FFFF
Stack area
File mapping area
Heap area
bss
data
rodata constants
Code segment
Low address 0x0000 0000

td>

Heap area Stack area
Management method The resources in the heap are controlled by the programmer (easy to produce memory leaks) The stack resources are automatically managed by the compiler, no manual control is required
Space size Non-continuous (the system uses a linked list to store free memory addresses) size is limited by the virtual memory size (32bit The system is theoretically smaller than 4G), usually larger The stack is a continuous memory area, the size is 1M or settings predetermined by the operating system, smaller
Growth direction The heap grows upward and toward higher addresses. The stack goes down and grows toward lower addresses.
Allocation method The heap is dynamically allocated (there is no statically allocated heap) Stack There are static allocation and dynamic allocation. Static allocation is done by the compiler (such as local variable allocation), and dynamic allocation is allocated by the alloca function. However, the dynamically allocated resources of the stack are released by the compiler, without the need for programmers to implement it!
Allocation efficiency The heap is provided by the C/C++ function library, and the mechanism is very complex. Therefore, the efficiency of the heap is much lower than that of the stack. The stack is a data structure provided by the system. The computer provides support for the stack at the bottom layer, allocates special registers to store the stack address, and has special instructions for stack operations.
Fragmentation problem For the heap, frequent new/delete will cause a lot of fragmentation and reduce program efficiency For the stack, it is somewhat similar to a first-in, last-out stack in the data structure. There is a one-to-one correspondence between in and out, and no fragmentation will occur.
Memory management mechanism The system has a linked list that records free memory addresses. When the system receives a program request, it traverses This linked list looks for the first heap node whose space is larger than the requested space, deletes the node in the free node linked list, and allocates the node space to the program (most systems will record this at the first address of this memory space. The size allocated once, so that delete can correctly release this memory space, and the system will put the excess part back into the free linked list); As long as the remaining space of the stack is greater than the requested space, the system will provide the program with memory, otherwise an exception will be reported and a stack overflow will be reported. (In this section, if you understand the difference between linked lists and queues, and the difference between discontinuous space and continuous space, it should be easier to understand the difference between these two mechanisms)