 Welcome. So today, we are going to talk about how we're going to implement this address-based abstraction that we sort of ended up with on Monday. So Monday, Carl walked through some of the problems with managing memory. And we came up with an idea, the idea of this address-based abstraction. The question is, can we actually get it to work? There's some nice properties that go along with it. And those are very desirable things in terms of how processes lay out memory and how the system manages it. The question is, what do we actually do to get this to work? So memory management is one of these fun topics where there's a great deal of software or hardware co-evolution that's happened over time. So software doing this on its own, not feasible. But with some hardware help and some of the hardware features that have evolved over time, we can do this. OK, so I'm at two checkpoints. How many people are done with the file system system calls? Oh, wow, OK. Something is different about this class than previous years. I don't know. Maybe you guys will all drop dead after assignment two and won't do anything else. But I hope not, because I would like to see everybody kind of do more things. But yeah, OK. So if you haven't finished the file system calls, you're a little bit behind at this point. You're a little behind the eight ball. You have about a week and a half left. And keep in mind, the deadline is right before spring break. So the earlier you finish, the earlier you can start spring breaking, whatever that means to you. And if you're on and you started to work on some of the process-related system calls, like you're in OK shape, leave some time for some of the debugging tasks and some of the air handling things. Because once you get fork and exec to work, you'll be able to run some of the bad call tests and some of the ran call tests. And those will probably start to expose some problems with your air handling and your other system calls. And you'll have to go back and sort of clean things up. Questions about logistics? Everything seems to be working? Forum is quiet. Office hours are busy, I hope. So yeah. Good luck. So we talked about, on Monday, you talked about a little bit about how address spaces are actually laid out. So remember, when we call exec, exec is what sets up the address space initially for a new process. And exec retrieves a blueprint from this binary file. So the ELF format file has structured information. I mean, essentially, this is a data structure that's been serialized into a file. And that information tells exec what data from the file should go where in the address space. Now, some of this layout is a function of convention. And some of it is actually done to help programmers. So for example, I won't give away the answer yet, why is the address zero? I've seen people doing this. Is the user address zero a valid address? Why not? What's wrong with zero? No? Is zero a valid virtual address? Yeah. Oh, yeah, right, yeah. Virtual, sorry. No, I'm just teasing. Let's try to be nicer. Ah, OK. Yeah, so zero's a valid address. Nothing wrong with zero. Something wrong with zero is just another friendly address that could potentially be valid. Now, zero is typically not a valid address. How many people have ever had a segmentation fault while trying to run C or C++ code or something like that? Yep. Now, 250 is coming back to mind. It's like, oh, sorry. So I'll give you guys a minute to get through the little PSD stuff that that causes. Yeah, so we typically don't, when we set up an address space. Remember, we got lots of room. We have 4GB, 2GB, something like that. We have a huge amount of space. We typically don't load stuff at address zero. Why? So address zero, while it could be a valid address, is typically not a valid address. Why not load stuff at zero? Yeah. Yeah, so one of the most frequent programming mistakes is a null pointer. Hi. A null pointer exception. So using a pointer that hasn't been initialized, you set up a pointer and then you called, for example, or using something where malloc fails or something. Anyway, you have a bad pointer. A lot of times those bad pointers end up pointing at zero. And it's not just zero that we leave empty. It's zero and then a huge swath, starting from zero, going up sometimes to like 40,000. Why is that? Why not just zero? I mean, if I just have zero, then I'm going to catch all those null pointer exceptions. And I'm only losing like 4 bytes of memory. Why would I reserve like a large chunk? Yeah. Yeah, so how many people when they've been, maybe they've been debugging your OS 161 kernel, you've seen like a bad address. It's like 12 or 4 or something like this. Like it's small. What is that? That is a function of how C does structure member dereferencing. So if you take a bogus pointer to a structure and you try to access one of the elements inside of it, the compiler takes that pointer and just does math. It says, OK, you wanted the third element of the structure and I know that the two elements that preceded are 4 bytes each. So I take that pointer and I add a bytes to it and that's how I get the address. So that's why you get those like small null pointer at. So if I leave a bunch of memory down there, if I make sure that all that memory down at zero is invalid, I can catch not only null pointer exceptions but null pointers that point to structures and a bunch of structure offsets and things like that. So this helps you with a lot of sort of typical programming mistakes. All right, yeah, so this type of thing. And this and now, hopefully, if you don't get anything else out of this class, you understand what a segmentation fault is. The error message that the compiler or the runtime throws up at you, or actually this is the operating system, is really archaic and weird. At some point, it will make a little bit more sense to you because we'll actually talk about segmentation and what that means. But this is what causes that problem. Is that the operating system has realized that a user program has tried to use an address that is not mapped. It doesn't know how to translate it. And so it barbs, OK. Yeah, OK, I'll skip this. So again, and I've seen people, but I have seen people do this, at the beginning of SysOpen, they say, is the path name null? So first of all, there's two problems. It can be null. I could fiddle with the linker and set it up so that I convince the system to load my global variables at address zero. That's perfectly valid. There's no rule that that can't be the case. And the other thing that I like about this is, how many different ways can that pointer be invalid? There's a user space pointer. How many different ways can it be bogus? What's that? No, no, but how many different bogus user pointers can the program pass you through this user pointer T? One? Well, like 2 to the 32, right? I mean, any address that the user passed could be bogus. How many of them have you eliminated by this check? One. So now there are 2 to the 32 minus 1 left that could still be wrong. So it's not worth it. Like it's not even worth the 10 cycles that you just consumed. It's like, oh, OK. Well, thanks. And then a copy in is like, oh, I appreciate that. I appreciate that you helped me out there. Just don't bother. This is wrong and broken. OK. So remember we talked about ways to allocate memory. So stack starts at the top. The address space grows down. The heap typically starts somewhere in the middle of the address space and grows up. The reason why this is OK is that I have a lot of address space. Remember, I'm allocating inside this 2 gigabyte or 4 gigabyte or sometimes now on machines with larger memory size, like 8 gigabyte, 16 gigabyte address space. So by the time the stack and the heap meet, something has gone wrong. Either I have an enormous heap, which is possible in certain cases, or I've done some really, really wild recursion that is just completely out of control. And probably actually by that point you've hit some sort of stack limit and the program has crashed. All right. The final thing I just want to talk about in terms of how things get set up in the address space is relocation, because we're not going to talk about this in this class, but it is a really interesting topic. So here's an interesting thing to think about. You know that your programs use libraries, these binary libraries. When we looked at the PMAP mappings before, we saw spaces in the address space that were reserved for those binary libraries. Here's the problem. Lots of different programs use those libraries. Do you think the libraries get loaded at the same address in every program? Every program in your system uses the C library. Probably 99.9%. Does the C library get loaded in the same part of the address space for every single program? I mean, that would be weird, right? That would mean every program would have to have reserved spaces for every library on the system, which I'm not even sure you have enough memory in the program for that. So when I load a shared object library, I actually have to do this process of rewriting a lot of the references inside that library so that it can be loaded into a particular part of memory. So that library has internal references. So for example, it wants to use a global variable inside the library, but it actually doesn't know where that global variable is going to be until the library is loaded. So there's this process of rewriting references inside the library once I know what the base address of the library is so that things can work. So this is kind of interesting. Not something we're going to talk about. So this is called, and again, this is called relocation. You'll be able to take code, move it somewhere else. And you can imagine doing that sort of messy. All right. So again, this address space abstraction sounds like it's a fantastic thing. And the only problem here is how do we do it? How can we actually achieve this? So this is our goal now. Our goal is to actually figure out how can we get this to work and work efficiently, because if it's slow as molasses, it's not all that help. All right. So what do we need here? So what are some of the things that sort of start to become obvious once you start to talk about address spaces? So if every user program, every process, has the same view of memory, it has this address space that starts with address zero and ends with address OX80 million on the MIPS and maybe higher a few years in other systems. What are some things that sort of logically follow from that? It's one fairly obvious thing. Once I have two programs that have, again, I'm giving them this address space abstraction. So both of them think that all the memory they use starts at zero, and they have zero to OX80 million all to use all by themselves. What does that mean? Why? Right, so one thing to keep in mind with virtual addresses is I've broken the uniqueness of memory addresses. When I talk about hardware addresses, those hardware addresses actually have to be unique. Once I start talking about virtual addresses, it's clear once I have two processes that virtual address zero is not sufficient to identify a byte of memory. Because process one has virtual address zero, and process two has virtual address zero, and process three has virtual address zero, et cetera. So clearly, I need some other information along with the process in order to make that memory address useful. So what else? What else am I trying to provide? I just leaked it. Multi-plex now. Yeah, I mean, keep in mind when people started using this abstraction, the address space abstraction, two gigabytes was way more than the amount of memory that you had on the physical machine. So even before I have two processes, even with a single process, I am promising it more memory than the physical machine has. Your little MIPS virtual machine, you can configure it with 16 megabytes of memory. And for assignment three, you're going to implement two gigabyte virtual address spaces. So clearly, I have to be able to do some multiplexing here. And it gets worse and worse as I add more processes. Because to some degree, I'm giving processes this illusion that they have access to all this memory. When in reality, the amount of physical memory on this system is quite a bit more limited. So not only do I have to multiplex physical memory, but you might start thinking about how can I provide even more space somewhere that I can use to sort of back up that physical memory once I've run out. Something else I have to accomplish is protection. I need to be able to make sure that the address spaces don't overlap and that processes don't accidentally share memory. Address spaces are supposed to be a protection boundary. To some degree, this is how the operating system isolates one process from another process. Is by making sure that a thread running in one process cannot, unless the processes have agreed and have set it up collaboratively, cannot peek into the memory being used by any other process or the current. So we talked about this. Yeah, so the idea of now I have this, I'm encouraging processes to spread out and use memory, I don't have very much. So now I've created not only a multiplexing challenge, but a management challenge. So how do I make sure that I put the small amount of memory that I have on the system to good use? OK. So and this gets in, and I think we've been hinting at this so far, right? And the thing that's fun about this is that the whole time you've been writing computer code, you guys have actually never used real memory. You've been living in the matrix. You've been using these things that the operating system said were memory addresses, but they were never actual memory addresses. They were just, you've been fooled. And we've done a very, very effective job of making sure that this is true, right? So now we're going to pull back the covers and talk about how this actually works. Yeah. Yeah, this is, sorry if this is the day when you realize that Santa was not real, right? I should have had like a trigger warning before this. OK. So anyway, so what are we going to do? Someone already hinted at this. We're going to create a mapping. We're going to introduce a level of indirection. We're going to translate these addresses that processes get to use, which we call virtual addresses, into the underlying physical addresses that represent actual memory on the system. Maybe. Or we might translate them into other things as well. We did this once before. So this is another case where we're adding a level of indirection. We did this like three weeks ago. Does anyone remember in what context? Yeah. Yeah, the idea of a file handle introduced another level of indirection. And some of the same goals apply here. Giving the process a reference and forcing the process to use the operating system to translate it gives us a lot of new features that we want when we manage memory. So if I give the process a reference to memory rather than an address of actual memory, I can take that reference away. So I can refuse to keep translating it. I can allow multiple processes to share memory by giving them two references that can be different that point to the same thing. So that's kind of cool. If two processes want to share memory, the same underlying physical memory can be mapped into two different places in their address space. That's kind of neat. I can move stuff around. This is kind of neat. If I give you a reference to something, as long as the reference continues to behave the same way. So as long as I give you a virtual address that keeps acting like memory, meaning that when I store something to it, I get that value back the next time I read. I mean, that's sort of what we expect memory to do. But as long as it keeps doing that, I can move the underlying contents of that memory around. So I could take something that you've been thinking, I've been using virtual address OX100, and I can point that to somewhere else, point it back. So I can move those chunks of memory around it will, as long as the reference continues to work and obeys the properties that I expect memory to have. And finally, in certain cases, I can use this feature to alter these references so that they point to things that aren't actually a memory. So you can use things like memory that aren't actually memory. So that's also kind of cool. And here's my little diagram saying the same thing that I just said, one of the least effective diagrams in the class, but anyway. OK. So let me emphasize this for a second. So what is the memory interface? Now, we don't typically think of everything else in the computer. Memory has an interface. Everything in a computer system has an interface. Sometimes the interface has really nice documentation somewhere, and sometimes we just take it for granted. But what is the memory interface? What's that? No. The interface to memory. Like, how do you use memory? What's that? No. No, no way. So let me ask a question. If every time you needed to use memory, you had to get the operating system involved, how slow would your program be? Really slow. Your program uses memory all the time. How? Yeah. That's how you get memory. But once I have memory, what do I do with it? What do I do with memory? This is not a trick question. Store, that's good. Like, var x equals 3. And then what else? Load. I mean, memory is a cache for information. So I write something to memory, and then I retrieve it later. So load from a particular address, and store to a particular address, a particular value. So what is memory supposed to do? This is the memory interface, but what are the semantics of these functions? It's memory. What does memory do? Yeah. It remembers the value, right? So again, like, oh man, this is really obvious. So anyway, let me give you an interview tip. If someone asks you an obvious question, answer it. Don't get in your own head. It might be as simple as you think. So yeah, I mean, again, this is basic, but maybe you guys never thought of it. After I store something to a memory address, I expect to get the same value back when I do a load. What happens if I do a load and I haven't stored anything to the address? What do I get back? Who knows? It's undefined. That's actually something to be careful about when you guys are doing your kernel programming. When you call malloc and you get a chunk of memory back, you've been provided permission to use that memory, but you haven't stored anything to it yet. So what are the contents of that memory potentially? Anything. Maybe David filled it with dead beef in malloc, or maybe it has some old, I'm serious, that's what he does to fill it with invalid addresses. Maybe it has the contents that it was holding when it was previously in use. Maybe it has zeros. Who knows? But the point is, it's undefined, typically not something that's guaranteed by this interface. So obviously, the address inspection requires that we break the direct connection with physical memory. So now, when we refer to addresses that are using this slightly different interface, we refer to them as virtual addresses. So physical addresses, when I talk about a physical memory address, I'm talking about memory. That address points to somewhere on that stick of memory that you bought. That's where it points. A virtual address, however, points to something that obeys the memory interface. It has load in store. And as long as you load from an address that you stored to previously, you are guaranteed to get the same result. There's actually one caveat to that. What is true about that? There's one little caveat to the memory interface. When I do a load from an address, I get the same value, except, oh, OK. No, that's not the answer I'm looking for. But that's also true. Yeah, that's just like a compiler hint. Well, come on. You guys know the answer to this question again. There we go. Right. If I reboot, yeah. I mean, that's kind of important, right? I mean, just for users of memory, keep in mind that memory is not permanent. So if I store something to memory and the machine reboots, I just have a chance to make a decision about whether or not it's going to translate it. I don't have to. There's no one forcing me to do this. So some virtual addresses I can give out that are only to be used by the kernel. So if a user program tries to use them, I can prevent it from using them and probably also terminate. And of course, I can also assign virtual addresses to different user programs so that they don't, they can't read or write each other's memory. That should have been on the slide as implied. I can also add additional levels of protection to these addresses because I'm doing the translation. I can say, this particular part of memory is read-only. This particular part of memory, in certain systems, you actually have write-only memory, which is an interesting concept that you might want to think about, why would that be useful? It is useful in certain cases. And I can also indicate that only certain parts of the address space can actually be used to store code, which is sort of useful, right? Yeah, duh. OK. Any questions at this point before we go on? Yeah. So we'll come back to this, right? So if that was the case, what would be wrong with virtual memory? Let's say that every time you touch memory, you have to go through the operations. Yeah, I mean, forget it. We would have done something else. They would never work. And remember all that context with overhead that we've talked about the past, saving all the state? Every time, you don't understand. Most of what programs do is moving data back and forth from memory, a lot of it. You shuffle things around in registers and then you run some computation and stuff like that. But a lot of the flow of data is not just within registers. It's also coming from and going back to memory. So if all of those operations required the kernel, user programs would be really slow. So yeah, we'll come back and talk a little bit about how we make this fast. Yeah. So that may be in the context of one of those broken approaches to doing memory management that you were talking about, right? Once I have virtual addresses, I certainly don't need to respect that. Oh, of course. Yeah. If you allocate a big array, that array can be all over the place in physical memory, right? And that's actually really important for avoiding fragmentation. And we'll come back when we talk about how memory gets allocated. Yeah, but that's a great point. That's another one of the games I can play for processes is that what looks contiguous to a process does not actually have to be contiguous because I can remap things. All right. So let's talk about a couple of places that virtual addresses come from. How do I get them? How does a process get accessed? So exec is one of the primary sources of setting up virtual address mappings. And it does that using this blueprint in the ELF file. So when you run exec, and you guys can look, there's code in your operating system. You don't have to write this happily because it's pretty boring. But there's a function called loadElf, and you can look through how it works. It's not that complicated. I mean, it goes through the Elf file. Each part of the Elf file says this much data from the file should go at this point in the address space. And so what loadElf does is it says, OK, the address space has to have valid addresses at this area. And then it reads the data in and puts it into that spot in the address space. And there are various ways that the Elf file can set up different regions depending on what's in them. So the code region is typically marked as read only after loadElf is done initializing it. The data might be marked as non-executable and things like this. These are newer conventions. So in the past, real killer programmers could write something called self-modifying code. Has anyone ever heard of self-modifying code? Yeah, this is just like that. And so can you still write self-modifying code? You probably have to set things up in a very odd way. Yeah, does anyone know what that is? Do you guys know what self-modifying code is? Yeah. So imagine that your source code as part of the process of running is actually rewriting itself. Does that make sense? So you loaded these instructions out of the file, but then at runtime those instructions are rewriting the instructions so that the code is changing over time. Yeah. Yeah, that's true. Yeah, but that's not. Just-in-time compilers are rewriting the byte code. Yeah, definitely. The just-in-time compiler is not rewriting the just-in-time compiler, right? That would be interesting. Yeah, but yeah, that's a form of this. But in this case, the actual binary itself is saying, OK. And I don't know how this works. There are algorithms out there apparently that are extremely efficient that you can write using self-modifying code. I've never tried it. OK. So just a review of what we saw with PMAP. I don't think I need to go over this unless there are questions. OK, fork. So another source of virtual addresses. So when fork copies the address space of the caller, it's essentially creating an entirely new set of virtual addresses for the new child. So the child starts with roughly the same virtual addresses as the parent. Maybe a couple of thread stacks got lost along the way. But all of those addresses have to point to new memory. Remember, to translate a virtual address, I had this tuple of process and virtual address. So I had process A virtual address X, process A virtual address Y. Now I have process B virtual address X, but those actually have to translate to different physical memory. So you can sort of convince yourself of that running these little toy programs. So here's an example. This is a, I think this will almost work. I think I actually might have run this. I think this actually does work. So what does this do? I initialize a value in memory. This is going to be somewhere on the stack of the thread that's running that I call fork. So who's running at the top? Who runs this code? This is the parent because it gets a non-zero return code. So what this does, what this code does is it prints, this particular printf statement prints the virtual address of the variable. So what is the child going to print right here? What is this printf in the child? If it prints 0x2010 here, what's it going to print here? Something different? It's a different process, but remember, I copied the virtual address mapping. It's a copy of the virtual addresses from the parent's address space. But it shouldn't be a copy of the physical addresses that the parent is using. So this prints the same virtual address. Remember, the parent and child. When I call fork, the child has the same virtual address mappings as the parent. That's why the child thinks that it has a copy of the parent's address space. However, what should this print down here? Three. And this is true no matter how these particular things interleave. The point is, my virtual addresses are the same. But the memory that they're pointing to is now private. If it wasn't private, let's say that this thread, let's say that this process ran first, and then this guy came down here, I would expect this to be 4 or something like that. So there's no race condition here. Once I call fork, the parent and child are modifying private memory. The memory mappings are the same. The memory that they point to is different. So if you just tried to print the value at i and the child, would it just print? No, so what should the value be here before I set it? It's 2. Because when I copied the memory, I copied those values. Yeah, yeah, exactly. So I copied the physical memory from the parent, maybe. There are some games I can play to avoid doing this. But if I did, I copied all that physical memory, so I got all those values. One of the pieces of physical memory I copied was this stack. And so I got the value of 2 in the right place. But once the parent and child are running separately, they're modifying private memory. Does this make sense? Confused? It's semi-confused, looks OK. I'll go on. It has this memory. Another way to create memory memory, new virtual addresses, is a system called Espreak. Does anyone know what Espreak does? Espreak. Espreak is used frequently by a library that you use. You don't typically call it yourself, but it gets called a lot. On your behalf, yeah. It is used by Malik. That's a good answer. Why? So think about it. Where did the addresses, so all of the addresses that are created, all the memory that's allocated, quote unquote, that's created by exec, is in the L file. But what if I need to allocate memory dynamically at runtime? And I want it to persist. So I can put it on my stack, but then when the function returns, it's gone. So where do I do dynamic memory allocations? I use libraries like Malik, but the way that Malik works is it asks the kernel for more memory. So Espreak is one of the ways that a process can ask the kernel for more memory. And Espreak is kind of a weird name for this call, but essentially what Espreak does is it asks the kernel to move the break point between the heap and the stack, essentially to move it typically up. So doing that allocates more space on the heap. So the first time you call Malik, Malik will call Espreak to get memory from the operating system, and then Malik manages that memory internally on your behalf. Writing Malik is a fun little, has anyone ever done that? Has anyone ever written an allocator? Just do this for fun. If you're going to write code in C for some reason, these are the kind of things you want to do. Don't try to write a web application in C. You'll just want to kill yourself. But if you're writing Malik, go for it. And Malik's a fun little problem. You can look at how our kernel Malik works. You have the source code for it. But Malik is a fun thing. And there are still people working on this problem. There are still new versions of Malik that have been coming out that have different features and stuff like that. So it's kind of a neat problem. So essentially, after I call Espreak, I have a bigger heap. And again, you typically don't call Espreak. But if you call Malik a lot and you're allocated more and more memory over time, the way that Malik got that memory initially was it called Espreak. And then it split the memory into little pieces and allocated it more effectively. But the way it got memory initially was from kernel Espreak. You can ask for a specific amount of additional memory. And it's not a granularity that we'll talk about a little bit. Malik doesn't ask for like 10 bytes. Malik will ask for a certain amount more. Is it possible to call Espreak? It's just a system call. You can call it directly. Now if you're using a Malik library that's also using Espreak, that may or may not confuse it. It might. It might not. I've also just discovered that apparently there's a way now to use M-Map to get additional memory that's not mapped to a file. And so I think modern Malik and limitations have started to use M-Map rather than Espreak. So M-Map, the last way we'll talk about of getting additional addresses, this is historically used to ask, like we talked about before, the operating system to map part of a file into my address space. So take a region of virtual addresses and have them point to a specific part of a file, maybe the whole file. And again, now there is a way, I think, starting in Linux 2.6 that you can call M-Map. But you can tell the kernel, I actually don't want this to map to a file. It doesn't have to persist. I just want a chunk of memory. Because M-Map actually allows me to request a particular part of my address space so I can actually allocate a chunk somewhere, whereas Espreak only moves this one break point back and forth. So M-Map's a little more flexible. All right. Where are we on time? I might have rambled too much. Oh, OK. I haven't been allowed. Great. So underneath the hood, so in order to understand memory management, at some level you can think about virtual addresses, but you also have to get down and think about the actual physical memory on this system and how the physical memory is actually allocated and managed. And this stuff gets really, really machine specific. So a lot of what, you know, a lot of the headache, I would argue, in booting up machines has to do with understanding the low level memory layout and where different pieces are allocated. So that's something that kernels do when they start up to figure out, OK, I'm running on this device, like, where are the physical addresses that I can use and things like this. Happily, the System 161 architecture that you need to understand is really simple. It's based on a 32-bit MIPS architecture, but it's actually, I think, simpler than that. I think it's been simplified a little bit. So it's a 32-bit architecture. So I have addresses from 0 to OX, F, F, F, F, F. And the MIPS architecture itself defines four address regions. So the first two gigabytes, this is a four-gigabyte address space, that's 2 to the 32, right? The first two gigabytes are for process virtual addresses. So these are addresses that the kernel has to translate. When a process tries to use one of these addresses, the kernel gets involved, at least the first time. The next 512 megabytes, so this is an interesting area, and this is sort of a historical artifact of this architecture. I'm not sure how common these areas are anymore. So this is what's called a direct mapped segment. So these addresses can only be used when I'm running in the kernel, but they are mapped by the memory management unit itself. So I don't have to do anything special to translate these addresses. The way they're translated is you subtract OX80 million, or you lop off the top bit. So for example, the address, OX80 million. If you guys, I mean, how many people poked around in your kernel and seen some of the addresses that your kernel is using internally for its own data structures, like thread pointers and stuff like that, they typically start OX80 million and then go on from there. And the reason is the kernel addresses that you're seeing are in this region. So the way I, how do I translate OX80 million? What physical address on the MIPS does that correspond to? Zero. So OX80 million 100 corresponds to physical address 100. So this gives the kernel a way to access physical memory directly without having to load any mappings. Does that make sense? So I can, when I'm running in the kernel, I can use these addresses and I can directly address hardware, the hardware memory. The next segment is called these kernel direct mapped addresses. These are uncached. These are essentially for devices. I won't bore you with the details. And then finally, there is one gigabyte of space on your machine for kernel virtual addresses. So kernel virtual addresses on the MIPS are exactly like process virtual addresses. They have to be translated. The only difference is they can only be used while you're in the kernel. Yeah. Yeah. Yeah. Then we're doing it through watching. Yeah, so OK. Let me point out a couple things. So this architecture came from a day when machines had like 32 megabytes of memory. So the amount of virtual addresses was way, way, way bigger than the amount of physical memory on the system. So how many people have 4 gigabytes or more of RAM on their computer or on your phone? So now we live in a different time. So this type of layout has a lot of problems on modern systems because it wastes a lot of memory. The reason that the MIPS is set up this way is because there are times when it is useful for the kernel to be able to access physical memory without requiring a translation. So let's say I booted up and I haven't loaded any translations yet. How do I load code from the kernel itself? Having these direct map addresses is helpful for that. So in certain cases, these help bootstrap certain operations on the system that wouldn't be able to proceed without. But it's a good question. But I also remember we have this whole set of kernel virtual addresses that I can use if I want. OK, I think I'm done. Carl will pick up on Friday. Good luck finishing up assignments.