prologue epilogue

本文通过使用GDB调试器来深入剖析C语言的内部机制,包括如何阅读和理解汇编代码,以及如何利用GDB来揭示静态局部变量的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

https://www.hackerschool.com/blog/7-understanding-c-by-learning-assembly
https://www.hackerschool.com/blog/5-learning-c-with-gdb

 

 

Last time, Alan showed how to use GDB as a tool to learn C. Today I want to go one step further and use GDB to help us understand assembly as well.

Abstraction layers are great tools for building things, but they can sometimes get in the way of learning. My goal in this post is to convince you that in order to rigorously understand C, we must also understand the assembly that our C compiler generates. I’ll do this by showing you how to disassemble and read a simple program with GDB, and then we’ll use GDB and our knowledge of assembly to understand how static local variables work in C.

Note: All the code in this post was compiled on an x86_64 CPU running Mac OS X 10.8.1 using Clang 4.0 with optimizations disabled (-O0).

Learning assembly with GDB

Let’s start by disassembling a program with GDB and learning how to read the output. Type the following program into a text file and save it as simple.c:

int main()
{
    int a = 5;
    int b = a + 6; return 0; } 

Now compile it with debugging symbols and no optimizations and then run GDB:1

$ CFLAGS="-g -O0" make simple
cc -g -O0    simple.c   -o simple
$ gdb simple

Inside GDB, we’ll break on main and run until we get to the return statement. We put the number 2 after next to specify that we want to run next twice:

(gdb) break main
(gdb) run
(gdb) next 2

Now let’s use the disassemble command to show the assembly instructions for the current function. You can also pass a function name to disassemble to specify a different function to examine.

(gdb) disassemble
Dump of assembler code for function main:
0x0000000100000f50 <main+0>:    push   %rbp
0x0000000100000f51 <main+1>: mov %rsp,%rbp 0x0000000100000f54 <main+4>: mov $0x0,%eax 0x0000000100000f59 <main+9>: movl $0x0,-0x4(%rbp) 0x0000000100000f60 <main+16>: movl $0x5,-0x8(%rbp) 0x0000000100000f67 <main+23>: mov -0x8(%rbp),%ecx 0x0000000100000f6a <main+26>: add $0x6,%ecx 0x0000000100000f70 <main+32>: mov %ecx,-0xc(%rbp) 0x0000000100000f73 <main+35>: pop %rbp 0x0000000100000f74 <main+36>: retq End of assembler dump. 

The disassemble command defaults to outputting instructions in AT&T syntax, which is the same syntax used by the GNU assembler.2 Instructions in AT&T syntax are of the format mnemonic source, destination. The mnemonic is a human readable name for the instruction. Source and destination are operands and can be immediate values, registers, memory addresses, or labels. Immediate values are constants, and are prefixed by a $. For instance, $0x5 represents the number 5 in hexadecimal. Register names are prefixed by a %.

Registers

It’s worth taking a quick detour to understand registers. Registers are data storage locations directly on the CPU. With some exceptions, the size, or width, of a CPU’s registers define its architecture. So if you have a 64-bit CPU, your registers will be 64 bits wide. The same is true of 32-bit CPUs (32-bit registers), 16-bit CPUs, and so on.3 Registers are very fast to access and are often the operands for arithmetic and logic operations.

The x86 family has a number of general and special purpose registers. General purpose registers can be used for any operation and their value has no particular meaning to the CPU. On the other hand, the CPU relies on special purpose registers for its own operation and the values stored in them have a specific meaning depending on the register. In our example above, %eax and %ecx are general purpose registers, while %rbp and %rsp are special purpose registers. %rbp is the base pointer, which points to the base of the current stack frame, and %rsp is the stack pointer, which points to the top of the current stack frame. %rbp always has a higher value than %rsp because the stack starts at a high memory address and grows downwards. If you are unfamiliar with the call stack, you can find a good introduction on Wikipedia.

One quirk of the x86 family is that it has maintained backwards compatibility all the way back to the 16-bit 8086 processor. As x86 moved from 16-bit to 32-bit to 64-bit, the registers were expanded and given new names so as to not break backwards compatibility with code that was written for older, narrower CPUs.

Take the general purpose register AX, which is 16 bits wide. The high byte can be accessed with the name AH, and the low byte with the name AL. When the 32-bit 80386 came out, the Extended AX register, or EAX, referred to the 32-bit register, while AX continued to refer to a 16-bit register that made up the lower half of EAX. Similarly, when the x86_64 architecture came out, the “R” prefix was used and EAX made up the lower half of the 64-bit RAX register. I’ve included a diagram below based on a Wikipedia article to help visualize the relationships I described:

|__64__|__56__|__48__|__40__|__32__|__24__|__16__|__8___|
|__________________________RAX__________________________|
|xxxxxxxxxxxxxxxxxxxxxxxxxxx|____________EAX____________|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|_____AX______|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|__AH__|__AL__|

Back to the code

This should be enough information to start reading our disassembled program.

0x0000000100000f50 <main+0>:    push   %rbp
0x0000000100000f51 <main+1>:    mov    %rsp,%rbp

The first two instructions are called the function prologue or preamble. First we push the old base pointer onto the stack to save it for later. Then we copy the value of the stack pointer to the base pointer. After this, %rbp points to the base of main’s stack frame.

0x0000000100000f54 <main+4>:    mov    $0x0,%eax

This instruction copies 0 into %eax. The x86 calling convention dictates that a function’s return value is stored in %eax, so the above instruction sets us up to return 0 at the end of our function.

0x0000000100000f59 <main+9>:    movl   $0x0,-0x4(%rbp)

Here we have something we haven’t encountered before: -0x4(%rbp). The parentheses let us know that this is a memory address. Here, %rbp is called the base register, and -0x4 is the displacement. This is equivalent to %rbp + -0x4. Because the stack grows downwards, subtracting 4 from the base of the current stack frame moves us into the current frame itself, where local variables are stored. This means that this instruction stores 0 at %rbp - 4. It took me a while to figure out what this line was for, but it seems that clang allocates a hidden local variable for an implicit return value from main.

You’ll also notice that the mnemonic has the suffix l. This signifies that the operands will be long (32 bits for integers). Other valid suffixes are byte, short, word, quad, and ten. If you see an instruction that does not have a suffix, the size of the operands are inferred from the size of the source or destination register. For instance, in the previous line, %eax is 32 bits wide, so the mov instruction is inferred to be movl.

0x0000000100000f60 <main+16>:   movl   $0x5,-0x8(%rbp)

Now we’re getting into the meat of our sample program! The first line of assembly is the first line of C in main and stores the number 5 in the next available local variable slot (%rbp - 0x8), 4 bytes down from our last local variable. That’s the location of a. We can use GDB to verify this:

(gdb) x &a
0x7fff5fbff768: 0x00000005
(gdb) x $rbp - 8
0x7fff5fbff768: 0x00000005

Note that the memory addresses are the same. You’ll notice that GDB sets up variables for our registers, but like all variables in GDB, we prefix it with a $ rather than the % used in AT&T assembly.

0x0000000100000f67 <main+23>:   mov    -0x8(%rbp),%ecx
0x0000000100000f6a <main+26>:   add    $0x6,%ecx
0x0000000100000f70 <main+32>:   mov    %ecx,-0xc(%rbp)

We then move a into %ecx, one of our general purpose registers, add 6 to it and store the result in %rbp - 0xc. This is the second line of C in main. You’ve maybe figured out that %rbp - 0xc is b, which we can verify in GDB:

(gdb) x &b
0x7fff5fbff764: 0x0000000b
(gdb) x $rbp - 0xc
0x7fff5fbff764: 0x0000000b

The rest of main is just cleanup, called the function epilogue:

0x0000000100000f73 <main+35>:   pop    %rbp
0x0000000100000f74 <main+36>:   retq   

We pop the old base pointer off the stack and store it back in %rbp and then retq jumps back to our return address, which is also stored in the stack frame.

So far we’ve used GDB to disassemble a short C program, gone over how to read AT&T assembly syntax, and covered registers and memory address operands. We’ve also used GDB to verify where our local variables are stored in relation to %rbp. Now we’re going to use our newly acquired skills to explain how static local variables work.

Understanding static local variables

Static local variables are a very cool feature of C. In a nutshell, they are local variables that only get initialized once and persist their values across multiple calls to the function where they are defined. A simple use case for static local variables is a Python-style generator. Here’s one that generates all of the natural numbers up to INT_MAX:

/* static.c */
#include <stdio.h>
int natural_generator()
{
        int a = 1; static int b = -1; b += 1; return a + b; } int main() { printf("%d\n", natural_generator()); printf("%d\n", natural_generator()); printf("%d\n", natural_generator()); return 0; } 

When compiled and run, this program prints the first three natural numbers:

$ CFLAGS="-g -O0" make static
cc -g -O0    static.c   -o static
$ ./static 1 2 3 

But how does this work? To understand static locals, we’re going to jump into GDB and look at the assembly. I’ve removed the address information that GDB adds to the disassembly so that everything fits on screen:

$ gdb static
(gdb) break natural_generator
(gdb) run
(gdb) disassemble
Dump of assembler code for function natural_generator:
push   %rbp
mov    %rsp,%rbp
movl   $0x1,-0x4(%rbp) mov 0x177(%rip),%eax # 0x100001018 <natural_generator.b> add $0x1,%eax mov %eax,0x16c(%rip) # 0x100001018 <natural_generator.b> mov -0x4(%rbp),%eax add 0x163(%rip),%eax # 0x100001018 <natural_generator.b> pop %rbp retq End of assembler dump. 

The first thing we need to do is figure out what instruction we’re on. We can do that by examining the instruction pointer or program counter. The instruction pointer is a register that stores the memory address of the next instruction. On x86_64, that register is %rip. We can access the instruction pointer using the $rip variable, or alternatively we can use the architecture independent $pc:

(gdb) x/i $pc
0x100000e94 <natural_generator+4>:  movl   $0x1,-0x4(%rbp)

The instruction pointer always contains the address of the next instruction to be run, which means the third instruction hasn’t been run yet, but is about to be.

Because knowing the next instruction is useful, we’re going to make GDB show us the next instruction every time the program stops. In GDB 7.0 or later, you can just run set disassemble-next-line on, which shows all the instructions that make up the next line of source, but we’re using Mac OS X, which only ships with GDB 6.3, so we’ll have to resort to the display command. display is like x, except it evaluates its expression every time our program stops:

(gdb) display/i $pc
1: x/i $pc  0x100000e94 <natural_generator+4>:  movl   $0x1,-0x4(%rbp)

Now GDB is set up to always show us the next instruction before showing its prompt.

We’re already past the function prologue, which we covered earlier, so we’ll start right at the third instruction. This corresponds to the first source line that assigns 1 to a. Instead of next, which moves to the next source line, we’ll use nexti, which moves to the next assembly instruction. Afterwards we’ll examine %rbp - 0x4 to verify our hypothesis that a is stored at %rbp - 0x4.

(gdb) nexti
7           b += 1;
1: x/i $pc  mov   0x177(%rip),%eax # 0x100001018 <natural_generator.b> (gdb) x $rbp - 0x4 0x7fff5fbff78c: 0x00000001 (gdb) x &a 0x7fff5fbff78c: 0x00000001 

They are the same, just as we expected. The next instruction is more interesting:

mov    0x177(%rip),%eax        # 0x100001018 <natural_generator.b>

This is where we’d expect to find the line static int b = -1;, but it looks substantially different than anything we’ve seen before. For one thing, there’s no reference to the stack frame where we’d normally expect to find local variables. There’s not even a -0x1! Instead, we have an instruction that loads 0x100001018, located somewhere after the instruction pointer, into %eax. GDB gives us a helpful comment with the result of the memory operand calculation and a hint telling us that natural_generator.b is stored at this address. Let’s run this instruction and figure out what’s going on:

(gdb) nexti
(gdb) p $rax
$3 = 4294967295
(gdb) p/x $rax
$5 = 0xffffffff

Even though the disassembly shows %eax as the destination, we print $rax, because GDB only sets up variables for full width registers.

In this situation, we need to remember that while variables have types that specify if they are signed or unsigned, registers don’t, so GDB is printing the value of %rax unsigned. Let’s try again, by casting %rax to a signed int:

(gdb) p (int)$rax
$11 = -1

It looks like we’ve found b. We can double check this by using the x command:

(gdb) x/d 0x100001018
0x100001018 <natural_generator.b>:  -1
(gdb) x/d &b
0x100001018 <natural_generator.b>:  -1

So not only is b stored at a low memory address outside of the stack, it’s also initialized to -1 before natural_generator is even called. In fact, even if you disassembled the entire program, you wouldn’t find any code that sets b to -1. This is because the value for b is hardcoded in a different section of the sample executable, and it’s loaded into memory along with all the machine code by the operating system’s loader when the process is launched.4

With this out of the way, things start to make more sense. After storing b in %eax, we move to the next line of source where we increment b. This corresponds to the next two instructions:

add    $0x1,%eax
mov    %eax,0x16c(%rip)        # 0x100001018 <natural_generator.b>

Here we add 1 to %eax and store the result back into memory. Let’s run these instructions and verify the result:

(gdb) nexti 2
(gdb) x/d &b
0x100001018 <natural_generator.b>:  0
(gdb) p (int)$rax
$15 = 0 

The next two instructions set us up to return a + b:

mov    -0x4(%rbp),%eax
add    0x163(%rip),%eax        # 0x100001018 <natural_generator.b>

Here we load a into %eax and then add b. At this point, we’d expect %eax to be 1. Let’s verify:

(gdb) nexti 2
(gdb) p $rax
$16 = 1

%eax is used to store the return value from natural_generator, so we’re all set up for the epilogue which cleans up the stack and returns:

pop    %rbp
retq   

Now we understand how b is initialized, let’s see what happens when we run natural_generator again:

(gdb) continue
Continuing.
1

Breakpoint 1, natural_generator () at static.c:5 5 int a = 1; 1: x/i $pc 0x100000e94 <natural_generator+4>: movl $0x1,-0x4(%rbp) (gdb) x &b 0x100001018 <natural_generator.b>: 0 

Because b is not stored on the stack with other local variables, it’s still zero when natural_generator is called again. No matter how many times our generator is called, b will always retain its previous value. This is because it’s stored outside the stack and initialized when the loader moves the program into memory, rather than by any of our machine code.

Conclusion

We began by going over how to read assembly and how to disassemble a program with GDB. Afterwards, we covered how static local variables work, which we could not have done without disassembling our executable.

We spent a lot of time alternating between reading the assembly instructions and verifying our hypotheses in GDB. It may seem repetitive, but there’s a very important reason for doing things this way: the best way to learn something abstract is to make it more concrete, and one of the best way to make something more concrete is to use tools that let you peel back layers of abstraction. The best way to to learn these tools is to force yourself to use them until they’re second nature.

  1. You’ll notice we’re using Make to build `simple.c` without a makefile. We can do this because Make has implicit rules for building executables from C files. You can find more information about these rules in the [Make manual](http://www.gnu.org/software/make/manual/make.html#Implicit-Rules).

  2. You can also have GDB output Intel syntax, which is used by NASM, MASM, and other assemblers, but that’s outside the scope of this post.

  3. Processors with SIMD instruction sets like MMX and SSE for x86 and AltiVec for PowerPC will often contain some registers that are wider than the CPU architecture.

  4. A discussion of object formats, loaders, and linkers is best saved for a future blog post.

Coming from a background in higher-level languages like Ruby, Scheme, or Haskell, learning C can be challenging. In addition to having to wrestle with C’s lower-level features like manual memory management and pointers, you have to make do without a REPL. Once you get used to exploratory programming in a REPL, having to deal with the write-compile-run loop is a bit of a bummer.

It occurred to me recently that I could use gdb as a pseudo-REPL for C. I’ve been experimenting with using gdb as a tool for learning C, rather than merely debugging C, and it’s a lot of fun.

My goal in this post is to show you that gdb is a great tool for learning C. I’ll introduce you to a few of my favorite gdb commands, and then I’ll demonstrate how you can use gdb to understand a notoriously tricky part of C: the difference between arrays and pointers.

An introduction to gdb

Start by creating the following little C program, minimal.c:

int main()
{
  int i = 1337;
  return 0; } 

Note that the program does nothing and has not a single printf statement.1 Behold the brave new world of learning C with gdb!

Compile it with the -g flag so that gdb has debug information to work with, and then feed it to gdb:

$ gcc -g minimal.c -o minimal
$ gdb minimal

You should now find yourself at a rather stark gdb prompt. I promised you a REPL, so here goes:

(gdb) print 1 + 2
$1 = 3

Amazing! print is a built-in gdb command that prints the evaluation of a C expression. If you’re unsure of what a gdb command does, try running help name-of-the-command at the gdb prompt.

Here’s a somewhat more interesting example:

(gbd) print (int) 2147483648
$2 = -2147483648

I’m going to ignore why 2147483648 == -2147483648; the point is that even arithmetic can be tricky in C, and gdb understands C arithmetic.

Let’s now set a breakpoint in the main function and start the program:

(gdb) break main
(gdb) run

The program is now paused on line 3, just before i gets initialized.Interestingly, even though i hasn’t been initialized yet, we can still lookat its value using the print commnd:

(gdb) print i
$3 = 32767

In C, the value of an uninitialized local variable is undefined, so gdb might print something different for you!

We can execute the current line with the next command:

(gdb) next
(gdb) print i
$4 = 1337

Examining memory with x

Variables in C label contiguous chunks of memory. A variable’s chunk is characterized by two numbers:

  1. The numerical address of the first byte in the chunk.

  2. The size of the chunk, measured in bytes. The size of a variable’s chunk is determined by the variable’s type.

One of the distinctive features of C is that you have direct access to a variable’s chunk of memory. The & operator computes a variable’s address, and the sizeof operator computes a variable’s size in memory.

You can play around with both concepts in gdb:

(gdb) print &i
$5 = (int *) 0x7fff5fbff584
(gdb) print sizeof(i)
$6 = 4 

In words, this says that i’s chunk of memory starts at address 0x7fff5fbff5b4 and takes up four bytes of memory.

I mentioned above that a variable’s size in memory is determined by its type, and indeed, the sizeof operator can operate directly on types:

(gdb) print sizeof(int)
$7 = 4
(gdb) print sizeof(double) $8 = 8 

This means that, on my machine at least, int variables take up fourbytes of space and double variables take up eight.

Gdb comes with a powerful tool for directly examing memory: the x command. The x command examines memory, starting at a particular address. It comes with a number of formatting commands that provide precise control over how many bytes you’d like to examine and how you’d like to print them; when in doubt, try running help x at the gdb prompt.

The & operator computes a variable’s address, so that means we can feed &i to x and thereby take a look at the raw bytes underlying i’s value:

(gdb) x/4xb &i
0x7fff5fbff584: 0x39    0x05    0x00    0x00

The flags indicate that I want to examine 4 values, formatted as hex numerals, one byte at a time. I’ve chosen to examine four bytes because i’s size in memory is four bytes; the printout shows i’s raw byte-by-byte representation in memory.

One subtlety to bear in mind with raw byte-by-byte examinations is that on Intel machines, bytes are stored in “little-endian” order: unlike human notation, the least significant bytes of a number come first in memory.

One way to clarify the issue would be to give i a more interesting value and then re-examine its chunk of memory:

(gdb) set var i = 0x12345678
(gdb) x/4xb &i
0x7fff5fbff584: 0x78 0x56 0x34 0x12 

Examining types with ptype

The ptype command might be my favorite command. It tells you the type of a C expression:

(gdb) ptype i
type = int
(gdb) ptype &i
type = int *
(gdb) ptype main
type = int (void)

Types in C can get complex but ptype allows you to explore them interactively.

Pointers and arrays

Arrays are a surprisingly subtle concept in C. The plan for this section is to write a simple program and then poke it in gdb until arrays start to make sense.

Code up the following arrays.c program:

int main()
{
    int a[] = {1,2,3}; return 0; } 

Compile it with the -g flag, run it in gdb, then next over the initialization line:

$ gcc -g arrays.c -o arrays
$ gdb arrays
(gdb) break main
(gdb) run
(gdb) next

At this point you should be able to print the contents of a and examine its type:

(gdb) print a
$1 = {1, 2, 3}
(gdb) ptype a
type = int [3] 

Now that our program is set up correctly in gdb, the first thing we should do is use x to see what a looks like under the hood:

(gdb) x/12xb &a
0x7fff5fbff56c: 0x01  0x00  0x00  0x00  0x02  0x00  0x00  0x00
0x7fff5fbff574: 0x03  0x00  0x00  0x00

This means that a’s chunk of memory starts at address 0x7fff5fbff5dc. The first four bytes store a[0], the next four store a[1], and the final four store a[2]. Indeed, you can check that sizeof knows that a’s size in memory is twelve bytes:

(gdb) print sizeof(a)
$2 = 12

At this point, arrays seem to be quite array-like. They have their own array-like types and store their members in a contiguous chunk of memory. However, in certain situations, arrays act a lot like pointers! For instance, we can do pointer arithmetic on a:

= preserve do
  :escaped
    (gdb) print a + 1
    $3 = (int *) 0x7fff5fbff570 

In words, this says that a + 1 is a pointer to an int and holds the address 0x7fff5fbff570. At this point you should be reflexively passing pointers to the x command, so let’s see what happens:

= preserve do
  :escaped
    (gdb) x/4xb a + 1
    0x7fff5fbff570: 0x02 0x00 0x00 0x00 

Note that 0x7fff5fbff570 is four more than 0x7fff5fbff56c, the address of a’s first byte in memory. Given that int values take up four bytes, this means that a + 1 points to a[1].

In fact, array indexing in C is syntactic sugar for pointer arithmetic: a[i] is equivalent to *(a + i). You can try this in gdb:

= preserve do
  :escaped
    (gdb) print a[0]
    $4 = 1
    (gdb) print *(a + 0) $5 = 1 (gdb) print a[1] $6 = 2 (gdb) print *(a + 1) $7 = 2 (gdb) print a[2] $8 = 3 (gdb) print *(a + 2) $9 = 3 

We’ve seen that in some situations a acts like an array and in others it acts like a pointer to its first element. What’s going on?

The answer is that when an array name is used in a C expression, it “decays” to a pointer to the array’s first element. There are only two exceptions to this rule: when the array name is passed to sizeof and when the array name is passed to the & operator.2

The fact that a doesn’t decay to a pointer when passed to the & operator brings up an interesting question: is there a difference between the pointer that a decays to and &a?

Numerically, they both represent the same address:

= preserve do
  :escaped
    (gdb) x/4xb a
    0x7fff5fbff56c: 0x01  0x00 0x00 0x00 (gdb) x/4xb &a 0x7fff5fbff56c: 0x01 0x00 0x00 0x00 

However, their types are different. We’ve already seen that the decayed value of a is a pointer to a’s first element; this must have type int *. As for the type of &a, we can ask gdb directly:

= preserve do
  :escaped
    (gdb) ptype &a
    type = int (*)[3]

In words, &a is a pointer to an array of three integers. This makes sense: a doesn’t decay when passed to &, and a has type int [3].

You can observe the distinction between a’s decayed value and &a by checking how they behave with respect to pointer arithmetic:

= preserve do
  :escaped
    (gdb) print a + 1
    $10 = (int *) 0x7fff5fbff570 (gdb) print &a + 1 $11 = (int (*)[3]) 0x7fff5fbff578 

Note that adding 1 to a adds four to a’s address, whereas adding 1 to &a adds twelve!

The pointer that a actually decays to is &a[0]:

= preserve do
  :escaped
    (gdb) print &a[0]
    $11 = (int *) 0x7fff5fbff56c 

Conclusion

Hopefully I’ve convinced you that gdb a neat exploratory environment for learning C. You can print the evaluation of expressions, examine raw bytes in memory, and tinker with the type system using ptype.

If you’d like to experiment further with using gdb to learn C, I have a few suggestions:

  1. Use gdb to work through the Ksplice pointer challenge.

  2. Investigate how structs are stored in memory. How do they compare to arrays?

  3. Use gdb’s disassemble command to learn assembly programming! A particularly fun exercise is to investigate how the function call stack works.

  4. Check out gdb’s “tui” mode, which provides a grahical ncurses layer on top of regular gdb. On OS X, you’ll likely need to install gdb from source.

Alan is a facilitator at Hacker School. He’d like to thank David Albert, Tom Ballinger, Nicholas Bergson-Shilcock, and Amy Dyer for helpful feedback.

Curious about Hacker School? Read about us, see what our alumni say, and apply to our fall batch.

  1. Depending on how aggressive your C compiler is about optimizing useless code, you may have to make it do something :) I tried running these examples on my Raspberry Pi and everything got compiled away.
  2. Formally, an array name is a “non-modifiable lvalue”. When used in an expression where an rvalue is required, an array name decays to a pointer to its first element. As for the exceptions, the & operator requires an lvalue and sizeof is just weird.

 

转载于:https://www.cnblogs.com/jvava/p/4220658.html

我已经确保了 gen_epilogue 生成退出标签​​ 并且保证在 gen_epilogue 中,​​先插入退出标签,再生成恢复指令 void CodeGen::gen_epilogue() { // TODO1:根据你的理解实现函数的 epilogue // 提示:可能包括的步骤:恢复ra、恢复s0、恢复sp、返回到调用方 // throw not_implemented_error{__FUNCTION__}; // 生成统一函数退出标签 std::string exit_label = func_exit_label_name(context.func); append_inst(exit_label, ASMInstruction::Label); // 标签必须在恢复指令前 // 恢复 ra 和 fp 寄存器 makeSureInRange("ld", RA_reg, SP, context.frame_size - 8, "ld"); makeSureInRange("ld", FP, SP, context.frame_size - 16, "ld"); // 恢复栈指针 makeSureInRange("addi", SP, SP, context.frame_size, "add"); // 返回调用方 append_inst("ret"); //TODO1-------------end } 但还是没有出现 main_exit 标签​​ 下面是完整代码 #include "CodeGen.hpp" #include "CodeGenUtil.hpp" #include <algorithm> #include <cstdint> #include <cstring> #include <string> #include <sys/types.h> #include <utility> #include <vector> void CodeGen::allocate() { unsigned offset = PROLOGUE_OFFSET_BASE; // 为每个参数分配栈空间(如果没有被分配寄存器) for (auto &arg : context.func->get_args()) { auto size = arg.get_type()->get_size(); offset = ALIGN(offset + size, size); context.offset_map[&arg] = -static_cast<int>(offset); } // 为每条指令结果分配栈空间 for (auto &bb : context.func->get_basic_blocks()) { for (auto &instr : bb.get_instructions()) { // 每个非 void 的定值都分配栈空间 if (not instr.is_void()) { auto size = instr.get_type()->get_size(); offset = ALIGN(offset + size, size); context.offset_map[&instr] = -static_cast<int>(offset); } // 为数组 alloca 分配额外空间(不对齐) if (instr.is_alloca()) { auto *alloca_inst = static_cast<AllocaInst *>(&instr); auto alloc_size = alloca_inst->get_alloca_type()->get_size(); offset += alloc_size; context.array_start_offset[alloca_inst] = offset; } } } // 最终的帧大小对齐为 16 的倍数 context.frame_size = ALIGN(offset, PROLOGUE_ALIGN); } void CodeGen::gen_prologue() { makeSureInRange("addi", SP, SP, -context.frame_size, "add"); makeSureInRange("sd", RA_reg, SP, context.frame_size - 8, "stx"); makeSureInRange("sd", FP, SP, context.frame_size - 16, "stx"); makeSureInRange("addi", FP, SP, context.frame_size, "add"); // 将函数参数转移到栈帧上 int garg_cnt = 0; int farg_cnt = 0; for (auto &arg : context.func->get_args()) { if (arg.get_type()->is_float_type()) { store_from_freg(&arg, FReg::fa(farg_cnt++)); } else { // int or pointer store_from_greg(&arg, Reg::a(garg_cnt++)); } } } void CodeGen::gen_epilogue() { // TODO1:根据你的理解实现函数的 epilogue // 提示:可能包括的步骤:恢复ra、恢复s0、恢复sp、返回到调用方 // throw not_implemented_error{__FUNCTION__}; // 生成统一函数退出标签 std::string exit_label = func_exit_label_name(context.func); append_inst(exit_label, ASMInstruction::Label); // 标签必须在恢复指令前 // 恢复 ra 和 fp 寄存器 makeSureInRange("ld", RA_reg, SP, context.frame_size - 8, "ld"); makeSureInRange("ld", FP, SP, context.frame_size - 16, "ld"); // 恢复栈指针 makeSureInRange("addi", SP, SP, context.frame_size, "add"); // 返回调用方 append_inst("ret"); //TODO1-------------end } // 将一个值 val 加载到目标通用寄存器 reg 中 void CodeGen::load_to_greg(Value *val, const Reg &reg) { assert(val->get_type()->is_integer_type() || val->get_type()->is_pointer_type()); if (auto *constant = dynamic_cast<ConstantInt *>(val)) {// 如果 val 是一个常数整数 int32_t val = constant->get_value(); if (IS_IMM_12(val)) { append_inst(ADDI, {reg.print(), "zero", std::to_string(val)}); } else { load_large_int32(val, reg);// 如果常数太大,用 load_large_int32 处理 } } else if (auto *global = dynamic_cast<GlobalVariable *>(val)) { // 如果是全局变量,生成地址加载指令 append_inst(LOAD_ADDR, {reg.print(), global->get_name()}); } else { //剩余情况从栈中加载到寄存器 load_from_stack_to_greg(val, reg); } } // 加载一个 32 位大整数到寄存器(通常是伪指令 li 会被展开成 lui+addi) void CodeGen::load_large_int32(int32_t val, const Reg &reg) { append_inst(LI, {reg.print(), std::to_string(val)}); } // 加载一个 64 位整数到寄存器,先加载高 32 位并左移,再加载低 32 位 void CodeGen::load_large_int64(int64_t val, const Reg &reg) { auto low_32 = static_cast<int32_t>(val & LOW_32_MASK); // 提取低 32 位 auto high_32 = static_cast<int32_t>(val >> 32); // 提取高 32 位 load_large_int32(high_32, reg); append_inst(SLLI, {reg.print(), reg.print(), "32"}); // 加载高 32 位并左移 32 位 load_large_int32(low_32, reg);// 覆盖写入低 32 位 } // 从栈帧中加载某个变量 val 到通用寄存器 reg 中 void CodeGen::load_from_stack_to_greg(Value *val, const Reg &reg) { // 获取该变量在当前函数栈帧中的偏移 auto offset = context.offset_map.at(val); auto offset_str = std::to_string(offset); auto *type = val->get_type(); // 获取该变量的类型(用于确定加载指令) if (IS_IMM_12(offset)) { // 如果 offset 能够用 12 位立即数表示,可以直接使用 offset(fp) 格式访问内存 if (type->is_int1_type()) { append_inst(LOAD_BYTE, {reg.print(), "fp", offset_str}); } else if (type->is_int32_type()) { append_inst(LOAD_WORD, {reg.print(), "fp", offset_str}); } else { // Pointer append_inst(LOAD_DOUBLE, {reg.print(), "fp", offset_str}); } } else { // 如果偏移过大,不能直接编码到指令中,先将 offset 加载到寄存器 load_large_int64(offset, reg); // reg = offset append_inst(ADD, {reg.print(), "fp", reg.print()}); // reg = fp + offset if (type->is_int1_type()) { append_inst(LOAD_BYTE, {reg.print(), reg.print(), "0"}); } else if (type->is_int32_type()) { append_inst(LOAD_WORD, {reg.print(), reg.print(), "0"}); } else { // Pointer append_inst(LOAD_DOUBLE, {reg.print(), reg.print(), "0"}); } } } // 将通用寄存器 reg 中的值存储到 val 对应的栈上位置(以 fp 为基址) void CodeGen::store_from_greg(Value *val, const Reg &reg) { // 获取该变量在当前函数栈帧中的偏移 auto offset = context.offset_map.at(val); auto offset_str = std::to_string(offset); auto *type = val->get_type(); // 获取该变量的类型(用于确定加载指令) if (IS_IMM_12(offset)) { // 如果 offset 能够用 12 位立即数表示,可以直接使用 offset(fp) 格式访问内存 if (type->is_int1_type()) { append_inst(STORE_BYTE, {reg.print(), "fp", offset_str}); } else if (type->is_int32_type()) { append_inst(STORE_WORD, {reg.print(), "fp", offset_str}); } else { // Pointer append_inst(STORE_DOUBLE, {reg.print(), "fp", offset_str}); } } else { // 对于 offset 超出立即数范围的情况,需要通过地址计算访问 auto addr = Reg::s(11); // 使用临时寄存器 s11 作为中间地址计算(可更换) load_large_int64(offset, addr); append_inst(ADD , {addr.print(), "fp", addr.print()}); if (type->is_int1_type()) { append_inst(STORE_BYTE, {reg.print(), addr.print(), "0"}); } else if (type->is_int32_type()) { append_inst(STORE_WORD, {reg.print(), addr.print(), "0"}); } else { // Pointer append_inst(STORE_DOUBLE, {reg.print(), addr.print(), "0"}); } } } // 将一个浮点类型的 Value 加载到浮点寄存器 freg 中 void CodeGen::load_to_freg(Value *val, const FReg &freg) { assert(val->get_type()->is_float_type()); if (auto *constant = dynamic_cast<ConstantFP *>(val)) { // 若是浮点常量,加载立即数 float val = constant->get_value(); load_float_imm(val, freg); } else { // 从栈中加载浮点变量 auto offset = context.offset_map.at(val); auto offset_str = std::to_string(offset); if (IS_IMM_12(offset)) { append_inst(FLOAD_SINGLE, {freg.print(), "fp", offset_str}); } else { // 偏移过大,使用寄存器间接寻址 auto addr = Reg::s(11); // 临时通用寄存器 s11 load_large_int64(offset, addr); // 加载偏移 append_inst(ADD, {addr.print(), "fp", addr.print()}); // addr = fp + offset append_inst(FLOAD_SINGLE, {freg.print(), addr.print(), "0"}); // 从 addr 加载 } } } // 将 float 常量加载进浮点寄存器 freg void CodeGen::load_float_imm(float val, const FReg &r) { int32_t bytes = *reinterpret_cast<int32_t *>(&val); // 将 float 解释为 32 位整数(IEEE 754 bit pattern) load_large_int32(bytes, Reg::s(11)); append_inst("fmv.s.x", {r.print(), Reg::s(11).print()}); // 使用 fmv.s.x 指令将整数位模式转成 float 放入 freg } // 将浮点寄存器 r 中的值存储回栈中 val 对应的位置 void CodeGen::store_from_freg(Value *val, const FReg &r) { auto offset = context.offset_map.at(val); if (IS_IMM_12(offset)) { auto offset_str = std::to_string(offset); append_inst(FSTORE_SINGLE, {r.print(), "fp", offset_str}); } else { // 偏移过大,需要间接寻址 auto addr = Reg::s(11); load_large_int64(offset, addr); append_inst(ADD, {addr.print(), "fp", addr.print()}); // addr = fp + offset append_inst(FSTORE_SINGLE, {r.print(), addr.print(), "0"}); // 从 r 存到 addr } } void CodeGen::gen_ret() { // TODO2:函数返回操作,你需要思考如何处理返回值(a0/fa0),如何返回到调用者(可以使用j指令、context中或许有你需要的信息) // throw not_implemented_error{__FUNCTION__}; // 如果有返回值 if (!context.inst->get_operands().empty()) { Value *ret_val = context.inst->get_operand(0); // 处理浮点返回值 if (ret_val->get_type()->is_float_type()) { load_to_freg(ret_val, FReg::fa(0)); } // 处理整型返回值 else { load_to_greg(ret_val, Reg::a(0)); } } // 直接跳转到退出标签,后续指令不再执行 append_inst("j " + func_exit_label_name(context.func)); // TODO2----------------end } void CodeGen::gen_br() { auto *branchInst = static_cast<BranchInst *>(context.inst); if (branchInst->is_cond_br()) { // TODO6:补全条件跳转操作 // 提示: 根据条件表达式的结果(reg t1 != 0),选择跳转到 true 分支或 false 分支。 // 你可能会用到blt、j等指令 // throw not_implemented_error{__FUNCTION__}; load_to_greg(branchInst->get_operand(0), Reg::t(1)); auto *truebb = static_cast<BasicBlock *>(branchInst->get_operand(1)); auto *falsebb = static_cast<BasicBlock *>(branchInst->get_operand(2)); append_inst("bnez t1, " + label_name(truebb)); append_inst("j " + label_name(falsebb)); // TODO6-------------------end } else { // 无条件跳转 auto *branchbb = static_cast<BasicBlock *>(branchInst->get_operand(0)); append_inst("j " + label_name(branchbb)); // 跳转到目标基本块 } } void CodeGen::gen_binary() { // 分别将左右操作数加载到 t0 t1 load_to_greg(context.inst->get_operand(0), Reg::t(0)); load_to_greg(context.inst->get_operand(1), Reg::t(1)); // 根据指令类型生成汇编 switch (context.inst->get_instr_type()) { case Instruction::add: output.emplace_back("add t2, t0, t1"); break; case Instruction::sub: output.emplace_back("sub t2, t0, t1"); break; case Instruction::mul: output.emplace_back("mul t2, t0, t1"); break; case Instruction::sdiv: output.emplace_back("div t2, t0, t1"); break; case Instruction::srem: output.emplace_back("remw t2, t0, t1"); break; default: assert(false); } // 将结果填入栈帧中 store_from_greg(context.inst, Reg::t(2)); } void CodeGen::gen_alloca() { auto *alloca_inst = static_cast<AllocaInst *>(context.inst); auto shuzu_offset = context.array_start_offset[alloca_inst]; std::string temp_reg = "t1"; // 加载偏移量到临时寄存器 load_large_int32(shuzu_offset, Reg::t(0)); // 计算栈地址:fp - shuzu_offset append_inst(SUB + string(" ") + temp_reg + ", fp, t0"); store_from_greg(context.inst, Reg::t(1)); } void CodeGen::gen_load() { auto ptr = context.inst->get_operand(0);//在指针类型auto*和auto没有任何区别 auto *type = context.inst->get_type(); load_to_greg(ptr, Reg::t(0)); std::string sreg ="t0"; if (type->is_float_type()) { std::string dest="ft0"; append_inst(FLOAD_SINGLE,{dest, sreg, "0"});//ft0=M[t0+0] store_from_freg(context.inst, FReg::ft(0)); } else { // TODO3: 补全load整型变量的情况,考虑int1 int32 int64 // throw not_implemented_error{__FUNCTION__}; // 根据变量类型选择加载指令 if (type->is_int1_type()) { append_inst("lb t1, 0(t0)"); } else if (type->is_int32_type()) { append_inst("lw t1, 0(t0)"); } else { // int64或指针 append_inst("ld t1, 0(t0)"); } store_from_greg(context.inst, Reg::t(1)); // TODO3----------------end } } void CodeGen::gen_store() { auto *type = context.inst->get_operand(0)->get_type();//怎么store取决于我们要存的数据是什么类型 auto *ptr = context.inst->get_operand(1);//位置 auto *data = context.inst->get_operand(0);//要存入的值 load_to_greg(ptr, Reg::t(1)); auto pst_reg=std::string("t1"); if (type->is_float_type()) { load_to_freg(data, FReg::ft(0)); append_inst(FSTORE_SINGLE ,{"ft0", pst_reg , "0"});//M[t1+0]=ft0 } else { if(type->is_int1_type()){ load_to_greg(data, Reg::t(0)); append_inst("sb "+std::string("t0")+", "+ "0("+pst_reg+")");//M[t1+0]=t0 }else if(type->is_int32_type()){ load_to_greg(data, Reg::t(0)); append_inst("sw "+std::string("t0")+", "+ "0("+pst_reg+")"); }else{ load_to_greg(data, Reg::t(0)); append_inst("sd "+std::string("t0")+", "+ "0("+pst_reg+")"); } } } void CodeGen::gen_icmp() { //这个指令有两个参数,就是两个参与运算的参数 auto sreg0=std::string("t0"); auto sreg1=std::string("t1"); load_to_greg(context.inst->get_operand(0), Reg::t(0)); // Operand 1 load_to_greg(context.inst->get_operand(1), Reg::t(1)); // Operand 2 auto dest_reg = std::string("t0"); // 根据指令类型生成汇编 switch (context.inst->get_instr_type()) { case Instruction::eq: append_inst("slt s11,"+sreg1+","+sreg0); append_inst("slt t0,"+sreg0+","+sreg1); append_inst("or t0,t0,s11"); append_inst("addi s11,zero,1"); append_inst("sub "+dest_reg+",s11,t0"); break; case Instruction::ne: append_inst("slt s11,"+sreg1+","+sreg0); append_inst("slt t0,"+sreg0+","+sreg1); append_inst("or "+dest_reg+",t0,s11"); break; case Instruction::gt: append_inst("slt "+dest_reg+","+sreg1+","+sreg0); break; case Instruction::ge: append_inst("slt "+dest_reg+","+sreg0+","+sreg1); append_inst("addi s11,zero,1"); append_inst("sub "+dest_reg+",s11,"+dest_reg); break; case Instruction::lt: append_inst("slt "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::le: append_inst("slt "+dest_reg+","+sreg1+","+sreg0); append_inst("addi s11,zero,1"); append_inst("sub "+dest_reg+",s11,"+dest_reg); break; default: assert(false); } store_from_greg(context.inst,Reg::t(0)); } void CodeGen::gen_fcmp() { // TODO7: 补全各种浮点型变量比较的情况(对应的Light IR:feq/fne/fgt/fge/flt/fle) // 提示: 你可能会用到 feq.s、flt.s、fle.s、xori等指令 // throw not_implemented_error{__FUNCTION__}; load_to_freg(context.inst->get_operand(0), FReg::ft(0)); load_to_freg(context.inst->get_operand(1), FReg::ft(1)); switch (context.inst->get_instr_type()) { case Instruction::feq: append_inst("feq.s t0, ft0, ft1"); break; case Instruction::fne: append_inst("feq.s t0, ft0, ft1"); append_inst("xori t0, t0, 1"); break; case Instruction::fgt: append_inst("flt.s t0, ft1, ft0"); break; case Instruction::fge: append_inst("fle.s t0, ft1, ft0"); break; case Instruction::flt: append_inst("flt.s t0, ft0, ft1"); break; case Instruction::fle: append_inst("fle.s t0, ft0, ft1"); break; default: assert(false); } store_from_greg(context.inst, Reg::t(0)); // TODO7----------------end } void CodeGen::gen_float_binary() { auto sreg0=std::string("ft0"); auto sreg1=std::string("ft1"); load_to_freg(context.inst->get_operand(0), FReg::ft(0)); // Operand 1 load_to_freg(context.inst->get_operand(1), FReg::ft(1)); // Operand 2 auto dest_reg = std::string("ft0"); // 根据指令类型生成汇编 switch (context.inst->get_instr_type()) { case Instruction::fadd: output.emplace_back("fadd.s "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::fsub: output.emplace_back("fsub.s "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::fmul: output.emplace_back("fmul.s "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::fdiv: output.emplace_back("fdiv.s "+dest_reg+","+sreg0+","+sreg1); break; default: assert(false); } // 将结果填入栈帧中 store_from_freg(context.inst,FReg::ft(0)); } void CodeGen::gen_zext() { auto sreg0=std::string("t0"); auto dest_reg = std::string("t0"); auto *type = context.inst->get_type(); if (type->is_float_type()) { sreg0=std::string("ft0"); load_to_freg(context.inst->get_operand(0), FReg::ft(0)); // Operand 1 dest_reg=std::string("ft0"); append_inst(GR2FR + string(" ")+sreg0+","+dest_reg);//放到合适的位置 } else { load_to_greg(context.inst->get_operand(0), Reg::t(0)); // Operand 1 if (type->is_int8_type()) { append_inst("andi " + dest_reg + ", " + sreg0 + ", 0xff"); } else if (type->is_int16_type()) { append_inst("andi " + dest_reg + ", " + sreg0 + ", 0xffff"); }else if(sreg0!=dest_reg){ append_inst("add "+dest_reg+", zero, "+sreg0); } } if(type->is_float_type()){ store_from_freg(context.inst,FReg::ft(0)); }else{ store_from_greg(context.inst,Reg::t(0)); } } void CodeGen::gen_call() { auto *callInst = static_cast<CallInst *>(context.inst); auto retType = callInst->get_function_type()->get_return_type(); int gregs = 0; // 通用寄存器参数计数器 int fregs = 0; // 浮点寄存器参数计数器 // 处理函数参数,按照类型加载到相应的寄存器 for (auto& arg : callInst->get_operands()) { auto argType = arg->get_type(); if (argType->is_float_type()) { load_to_freg(arg, FReg::fa(fregs++)); // 加载到浮点寄存器 } else if (argType->is_pointer_type() || argType->is_integer_type()) { load_to_greg(arg, Reg::a(gregs++)); // 加载到通用寄存器 } } // 生成函数调用指令 append_inst("jal " + callInst->get_operand(0)->get_name()); // 根据返回值类型选择寄存器存储返回值 if (retType->is_float_type()) { store_from_freg(callInst, FReg::fa(0)); // 浮点返回值 } else if (retType->is_integer_type()) { store_from_greg(callInst, Reg::a(0)); // 整数返回值 } } /* * %op = getelementptr [10 x i32], [10 x i32]* %op, i32 0, i32 %op //多维数组访问 * %op = getelementptr i32, i32* %op, i32 %op //一维数组/直接访问指针 * * Memory layout * - ^ * +-----------+ | 低地址 * | arg ptr |---+ | //arg ptr 是你传给 GEP 的起始指针(基准地址) * +-----------+ | | * | | | | * +-----------+ / | * | |<-- | * | | \ | * | | | | //Array 是连续内存的数组区域,GEP 会根据偏移量在这里面计算具体元素地址。 * | Array | | | * | | | | * | | | | * | | | | * +-----------+ | | * | Pointer |---+ | //Pointer 表示计算完地址后的结果,即 GEP 的结果(往往是你要访问或存储的内存地址)。 * +-----------+ | * | | | * +-----------+ | * | | | * +-----------+ | * | | | * +-----------+ | 高地址 * + */ void CodeGen::gen_gep() { auto *gepInst = static_cast<GetElementPtrInst *>(context.inst); int len=gepInst->get_num_operand(); // 操作数个数,包含指针 + 若干维度的下标 std::vector<Value *> ops=gepInst->get_operands(); // 获取所有操作数 //拿到基准地址->拿到值->基准地址修改一下->存回去 if(len>=3){ // TODO9: 完善多维数组地址计算,形如 a[i][j] 、 a[i][j][k]等形式的访问 // 提示:1. 操作数从第二个开始处理即可,第一个操作数是基准指针,后面的操作数都表示下标, // 2. 具体可以用context.inst->get_operand(j)获取第j+1个操作数。 // 3. 依次处理每一维度下标,将其乘以对应元素大小,累加偏移量。 // 需要考虑元素大小超过imm12范围的情况,比如int a[2][300][300];时, // 处理第一维时,每个 a[i] 是一个 300x300 的二维数组,共 360000 字节,超过imm12 // 4. 将偏移量加到基准指针上,得到最终地址。并存入当前指令目标变量对应的栈帧位置 // throw not_implemented_error{__FUNCTION__}; // 处理多维数组 load_to_greg(gepInst->get_operand(0), Reg::t(1)); // 基地址 Type *current_type = gepInst->get_element_type(); // 基类型 append_inst("addi t2, zero, 0"); // 初始化总偏移量 for (int j = 1; j < len; j++) { // 推导当前维度类型 if (current_type->is_array_type()) { current_type = current_type->get_array_element_type(); } else if (current_type->is_pointer_type()) { current_type = current_type->get_pointer_element_type(); } // 获取元素大小 int step = current_type->get_size(); // 处理下标 load_to_greg(gepInst->get_operand(j), Reg::t(0)); load_large_int32(step, Reg::t(3)); append_inst("mul t0, t0, t3"); append_inst("add t2, t2, t0"); } // 计算最终地址 append_inst("add t0, t1, t2"); store_from_greg(context.inst, Reg::t(0)); // TODO9-------------------end }else{//形如a[i]的访问,或访问指针 auto dest_reg=std::string("t0"); auto *ptr = context.inst->get_operand(0); // 指针 auto ptr_reg=std::string("t1"); load_to_greg(ptr, Reg::t(1)); // 加载基准地址 auto *idx = context.inst->get_operand(1); // 下标 auto idx_reg=std::string("t0");//这个是常数,也就是数组下标 load_to_greg(idx, Reg::t(0)); // 加载下标值 // 以下三条指令实现乘以 4(即元素大小为 4 的简化情况): append_inst("add s11,"+idx_reg+" , "+idx_reg); // s11 = 2 * idx append_inst("add s11,s11,s11");// s11 = 4 * idx // t0 = ptr_reg + s11,即最终地址 = 原始地址 + 偏移 append_inst("add "+dest_reg+",s11,"+ptr_reg); //把t0里存的最终地址存回栈帧的对应位置 //比如当前IR是 %op0 = getelementptr xxxxxx ,那这里就是把计算得到的地址存到%op0在栈帧中的位置 store_from_greg(context.inst, Reg::t(0)); } } void CodeGen::gen_sitofp() { auto *itfInst = static_cast<SiToFpInst *>(context.inst); std::vector<Value *> ops=itfInst->get_operands(); auto sreg0=std::string("t0"); load_to_greg(context.inst->get_operand(0),Reg::t(0)); auto dest_reg= std::string("ft0"); append_inst(GR2FR ,{dest_reg, sreg0}); store_from_freg(context.inst,FReg::ft(0)); } void CodeGen::gen_fptosi() { // TODO8: 浮点数转向整数,注意向下取整(rtz),你可能会用到指令fcvt.w.s // throw not_implemented_error{__FUNCTION__}; load_to_freg(context.inst->get_operand(0), FReg::ft(0)); append_inst("fcvt.w.s t0, ft0, rtz"); // 使用rtz舍入模式 store_from_greg(context.inst, Reg::t(0)); // TODO8--------------------end } void CodeGen::global_array_int(ConstantArray * init_val){//全局整型数组变量 /*示例输出 int a[5]={0,1,2,3,4}; .data .globl a .align 3 .type a, @object .size a, 20 a: .word 0 .word 1 .word 2 .word 3 .word 4 */ for (unsigned i = 0; i < init_val->get_size_of_array(); i++) {//获得这一层的大小 Constant *element = init_val->get_element_value(i); if (!dynamic_cast<ConstantArray *>(element)) {//这个元素已经不再是array了 auto *IntVal = static_cast<ConstantInt *>(element); append_inst(".word", {std::to_string(IntVal->get_value())}, ASMInstruction::Atrribute); }else{ //这个元素依然是array,递归下去 auto new_array=static_cast<ConstantArray *>(element); global_array_int(new_array); } } } void CodeGen::global_array_float(ConstantArray * init_val){ /*示例输出 float a[3]={1.01,4.11,13.99}; .data .globl a .align 3 .type a, @object .size a, 12 a: .word 1065437102 //float 1.01 .word 1082361119 //float 4.11 .word 1096800010 //float 13.99 */ // TODO5-2:完善浮点型全局数组变量初始化 // 提示:可以参考global_array_int的实现 // throw not_implemented_error{__FUNCTION__}; for (unsigned i = 0; i < init_val->get_size_of_array(); i++) { Constant *element = init_val->get_element_value(i); if (!dynamic_cast<ConstantArray *>(element)) { auto *FPVal = static_cast<ConstantFP *>(element); float val = FPVal->get_value(); int32_t bytes = *reinterpret_cast<int32_t*>(&val); append_inst(".word", {std::to_string(bytes)}, ASMInstruction::Atrribute); } else { global_array_float(static_cast<ConstantArray *>(element)); } } // TODO5-2------------------end } void CodeGen::run() { // 确保每个函数中基本块的名字都被设置好 m->set_print_name(); /* 使用 GNU 伪指令为全局变量分配空间 * 你可以使用 `la` 指令将标签 (全局变量) 的地址载入寄存器中, 比如 * 要将 `a` 的地址载入 t0, 只需要 `la t0, a` * 由于在IR自动化生成阶段,我们为无初始值的全局变量分配了0作为初始值,因此在目标代码生成阶段,全局变量都有初始值 */ if (!m->get_global_variable().empty()) { append_inst("Global variables", ASMInstruction::Comment); /* * 虽然可以使用 `.bss` 伪指令为未初始化数据分配空间, * 我们依然显式指定 `.data` 段,这是因为: * * - `.data` 更加通用,与标准 RISC-V 编译器行为一致; * - `.bss` 虽然常用于未初始化数据,但某些旧版本 GNU 汇编器对其支持不完善; * - 显式使用 `.data` 能更好地控制输出段结构。 */ append_inst(".text", ASMInstruction::Atrribute); append_inst(".data",ASMInstruction::Atrribute); for (auto &global : m->get_global_variable()) { //给全局变量分配空间 if(global.get_type()->get_pointer_element_type()->is_integer_type()){//处理整数型全局变量 auto *IntVal = static_cast<ConstantInt *>(global.get_init()); /* 输出形式示例: .globl a .align 2 .type a, @object .size a, 4 a: .word 5 */ auto size = global.get_type()->get_pointer_element_type()->get_size(); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 2", ASMInstruction::Atrribute); // 对齐到 4 字节 append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), std::to_string(size)}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); append_inst(".word", {std::to_string(IntVal->get_value())}, ASMInstruction::Atrribute); }else if(global.get_type()->get_pointer_element_type()->is_array_type()){ //处理数组类型全局变量 /* 输出形式示例: .globl a .data .align 3 .type a, @object .size a, 20 a: .word 0 .word 1 .word 2 .word 3 .word 4 */ if(global.get_type()->get_pointer_element_type()->get_array_element_type()->is_integer_type()){ //整型数组 auto size = global.get_type()->get_pointer_element_type()->get_size(); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 3", ASMInstruction::Atrribute); // 对齐到 8 字节 append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), std::to_string(size)}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); if(dynamic_cast<ConstantZero *>(global.get_init())){ // 初始化值为 0,使用 `.space` 节省空间 append_inst(".space", {std::to_string(size)}, ASMInstruction::Atrribute); }else{ //如果不是0 auto *IntVal = static_cast<ConstantArray *>(global.get_init()); global_array_int(IntVal); } }else{ //浮点型数组 // TODO5-1:完善浮点型全局数组变量声明及其初始化 // 提示:你可能需要将初始化值不为0的浮点型全局数组变量的处理逻辑封装到global_array_float函数中,因此可能需要完成TODO5-2,即填充global_array_float函数 // 当然你也可以直接在当前else分支内处理,弃用global_array_float函数(对应的TODO5-2也不用完成) // throw not_implemented_error{__FUNCTION__}; auto size = global.get_type()->get_pointer_element_type()->get_size(); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 3", ASMInstruction::Atrribute); // 8字节对齐 append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), std::to_string(size)}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); if (dynamic_cast<ConstantZero *>(global.get_init())) { append_inst(".space", {std::to_string(size)}, ASMInstruction::Atrribute); } else { auto *FPVal = static_cast<ConstantArray *>(global.get_init()); global_array_float(FPVal); } // TODO5-1------------------end } }else if(global.get_type()->get_pointer_element_type()->is_float_type()){ //浮点型全局变量 /* 输出形式示例: float a=1.01; .globl a .align 2 .type a, @object .size a, 4 a: .word 1065437102 // float 1.01 */ // TODO4:完善浮点型全局变量声明及其初始化 // 提示:RISC-V 中没有 .float 指令,需手动将 float 转换为 int 再用 .word 表示原始比特位 // 可以使用 reinterpret_cast<int&>(float) 实现 float → int 的位级转换 // throw not_implemented_error{__FUNCTION__}; auto *FPVal = static_cast<ConstantFP *>(global.get_init()); float val = FPVal->get_value(); int32_t bytes = *reinterpret_cast<int32_t*>(&val); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 2", ASMInstruction::Atrribute); append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), "4"}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); append_inst(".word", {std::to_string(bytes)}, ASMInstruction::Atrribute); // TODO4--------------------------end } } } // 函数代码段 append_inst(".text", ASMInstruction::Atrribute); append_inst(".align 2",ASMInstruction::Atrribute); for (auto &func : m->get_functions()) { if (not func.is_declaration()) { // 更新 context context.clear(); context.func = &func; // 函数信息 append_inst(".globl", {func.get_name()}, ASMInstruction::Atrribute); append_inst(".type", {func.get_name(), "@function"}, ASMInstruction::Atrribute); append_inst(func.get_name(), ASMInstruction::Label); // 分配函数栈帧 allocate(); // 生成 prologue gen_prologue(); //处理bb for (auto &bb : func.get_basic_blocks()) { context.bb = &bb; append_inst(label_name(context.bb), ASMInstruction::Label); for (auto &instr : bb.get_instructions()) { // For debug append_inst(instr.print(), ASMInstruction::Comment); context.inst = &instr; // 更新 context switch (instr.get_instr_type()) { case Instruction::ret: gen_ret(); break; case Instruction::br: gen_br(); break; case Instruction::add: case Instruction::sub: case Instruction::mul: case Instruction::sdiv: case Instruction::srem: gen_binary(); break; case Instruction::fadd: case Instruction::fsub: case Instruction::fmul: case Instruction::fdiv: gen_float_binary(); break; case Instruction::alloca: gen_alloca(); break; case Instruction::load: gen_load(); break; case Instruction::store: gen_store(); break; case Instruction::ge: case Instruction::gt: case Instruction::le: case Instruction::lt: case Instruction::eq: case Instruction::ne: gen_icmp(); break; case Instruction::fge: case Instruction::fgt: case Instruction::fle: case Instruction::flt: case Instruction::feq: case Instruction::fne: gen_fcmp(); break; case Instruction::phi: break; case Instruction::call: gen_call(); break; case Instruction::getelementptr: gen_gep(); break; case Instruction::zext: gen_zext(); break; case Instruction::fptosi: gen_fptosi(); break; case Instruction::sitofp: gen_sitofp(); break; default: assert(false && "Unhandled instruction type"); } } } // 生成 epilogue gen_epilogue(); } } } std::string CodeGen::print() const { std::string result; for (const auto &inst : output) { result += inst.format(); } auto sub = result.find("memset_int"); while (sub != string::npos) { result.replace(sub, 10, "memset"); sub = result.find("memset_int"); } sub = result.find("memset_float"); while (sub != string::npos) { result.replace(sub, 12, "memset"); sub = result.find("memset_float"); } return result; }
最新发布
05-29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值