It is the 1727th of March 2020 (aka the 21st of November 2024)
You are 18.191.195.105,
pleased to meet you!
mailto:blog-at-heyrick-dot-eu
Making ROMs writeable
In this little bit of geekery, we are going to do the impossible. We are going to make the ROM writeable!
Actually, it is not that complicated. You see, in all of the modern SoC ARM boards, the ROM image is loaded into a bit of memory which is then marked as being Read Only. So we just need to work out which part of the memory mapping to fiddle to Read/Write.
Why?
If you're asking that, you're new around here. There is no "Why". ☺
IMPORTANT NOTE: This has been tested on a Pi (1). It may or may not work on any other ARMv6 board. Unfortunately the page tables and MMU are one of the things that ARM has a propensity for changing, so there is no guarantee that this will work on other devices.
First, we need a few pieces of information. We need to know the address of the start of the ROM image. We need to know the location of the Level 1 page tables, and then we need to know where in the page table points to the ROM.
First up is the ROM base address. This can be easily determined by looking for the address of the first module (UtilityModule) and masking out the offset (as UtilityModule follows the hidden RISC OS module, which follows the HAL and boot code).
>SYS "OS_Module", 12, 0 TO ,,,mod%
>rom% = (mod% AND &FF000000)
>PRINT ~rom%
FC000000
>
Now we need to determine the location of the Level 1 page table. In RISC OS 5 this is available via OS_Memory as follows:
Finally, since the Level 1 page table is linear (a word per megabyte), we need to work out the offset:
pokeaddr% = l1pt% + ((rom% >>> 20) << 2)
We shift >>>20 to change the ROM address into a number of megabytes by shifting the value 20 places to the right. Note the use of three > symbols. This is because the original address (&FC000000) would appear as a negative number to BASIC. Had we used the usual >>, the result would have preserved the sign bit across the shifting, leading to the invalid result &FFFFFFC0. By using >>>, we tell BASIC to ignore the sign bit, providing us with the desired result of &FC0.
The final << 2 then shifts the address back up two places, to word align it.
Yes, we could have just shifted by 18, but this 20 then 2 method makes it clearer what is going on, if you know that 20 to the right converts to megabytes and 2 the the left word aligns.
For what it is worth, the eventual address is &FAC03F00 - however you must always perform this calculation instead of making assumptions because the address was &FB403F00 in RISC OS 5.21.
Now we have an address, what do we do with it? If you enter *Memory FAC03F00 +4, you will see the result showing the value &0DB0880E.
To understand what is here, we must now turn to an ARM datasheet for the ARMv6 architecture:
The value &0DB0880E is 1101101100001000100000001110 in binary. So let us merge the binary value into the section description, to explain what is going on:
This location may be buffered and cached (B + C = 1), but no write allocate (TEX = 0). It is not shared (S = 0), it is globally allocated (nG = 0), and my doc says "NS" should be zero, as it is.
The important part, however, is the APX bit (which is 1) and the AP bits (which are 10). Taken together, 1 10 means that the access permission is Read only in both privileged and user mode.
We must always check that the lowest two bits are 10 and that bit 18 is 0. This should be the case, as it indicates that this descriptor is a regular 1MiB section. The beauty of the ARM's MMU design is that the level 1 page table can hold different types of descriptor, such as a 16MiB "supersection", as well as entries into a second level page table. So by ensuring that the data is something we can recognise, we can reject unknown behaviour in the future or on other processors.
So now some code to read the bit of memory, and check the page table descriptor is the correct type. It's a little bit complicated as we cannot read the descriptor in User mode, so we toddle off to assembler.
It is important to note that calling OS_EnterOS, then trying to read the word using something like memword% = pokeaddr%!0 will hang the computer. BASIC doesn't much like operating in a privileged mode.
No worries. This'll do it (DIM code% 63 at the start):
Setting A% has the side effect of setting R0. Calling the code with USR (instead of CALL) means we expect to see a result in R0 on exit. The code itself is dead easy - we enter SVC mode, we read the word pointed to by R0 into R0, we come out of SVC mode and back into USR mode, and finally return back to BASIC.
To validate, then:
IF ( (((memword% >> 18) AND 1) <> 0) AND ((memword% AND 3) <> 2) ) THEN
PRINT "Unknown page descriptor."
END
ENDIF
Now comes the good stuff. In order to make the ROM writeable, we need to clear the APX bit and set both of the AP bits.
memword% = memword% EOR (1 << 15) : REM Clear APX
memword% = memword% OR (3 << 10) : REM Set both AP bits
This should yield the value &0DB00C0E, which now needs to be written back to the page table. Here's some code to do that, it's a lot like the last piece of assembler:
At this point, the first megabyte of the ROM will now be writeable. We can test this as follows:
checkaddr% = rom% + &10000
check% = checkaddr%!0
IF (check% <> &6D49534F) THEN
PRINT "Unable to check ROM is writeable."
END
ENDIF
checkaddr%!0 = &4B434952
check% = checkaddr%!0
IF (check% <> &4B434952) THEN
PRINT "ROM is *NOT* writeable."
END
ENDIF
checkaddr%!0 = &6D49534F
PRINT "Success!"
END
The first part of the RISC OS image, after the HAL, is at offset +&10000. The first word is "OSIm". We temporarily change this to "RICK", check it, then change it back.
Note that this may sometimes fail with an abort on data transfer. I think the MMU logic needs time or TLB flush or something. Trying it again usually works.
Here, then, is the full listing:
REM >ROMhack
REM Make the first megabyte of Pi 1's ROM writeable!
REM
REM By Rick Murray, 2016/08/03
REM
REM Open Source software - this code has been released under the EUPL v1.1 (only).
REM https://joinup.ec.europa.eu/community/eupl/og_page/european-union-public-licence-eupl-v11
REM
DIM code% 63
REM Work out where the ROM begins
SYS "OS_Module", 12, 0 TO ,,,mod%
rom% = (mod% AND &FF000000)
REM Read the address of the level 1 page table
SYS "OS_Memory", 16 + ( 7 << 8) TO ,l1pt%
REM Calculate our offset into the page table
pokeaddr% = l1pt% + ((rom% >>> 20) << 2)
REM Read the descriptor
P% = code%
[ OPT 2
SWI "OS_EnterOS"
LDR R0, [R0]
SWI "OS_LeaveOS"
MOV PC, R14
]
A% = pokeaddr%
memword% = USR(code%)
REM Validate the descriptor
IF ( (((memword% >> 18) AND 1) <> 0) AND ((memword% AND 3) <> 2) ) THEN
PRINT "Unknown page descriptor."
END
ENDIF
REM Give this descriptor Read/Write (all modes) permission
memword% = memword% EOR (1 << 15) : REM Clear APX
memword% = memword% OR (3 << 10) : REM Set both AP bits
REM Write it back
P% = code%
[ OPT 2
SWI "OS_EnterOS"
STR R1, [R0]
SWI "OS_LeaveOS"
MOV PC, R14
]
A% = pokeaddr%
B% = memword%
CALL code%
REM Verify it worked... this may sometimes fail so this part is
REM OPTIONAL and can be omitted if so desired.
checkaddr% = rom% + &10000
check% = checkaddr%!0
IF (check% <> &6D49534F) THEN
PRINT "Unable to check ROM is writeable."
END
ENDIF
checkaddr%!0 = &4B434952
check% = checkaddr%!0
IF (check% <> &4B434952) THEN
PRINT "ROM is *NOT* writeable."
END
ENDIF
checkaddr%!0 = &6D49534F
PRINT "Success!"
END
As before, homework for the user:
Hard: Figure out what to flush to ensure the permission bits are noted by the MMU so the subsequent access doesn't fail.
Easy: Wrap the code in some looping logic to step through all five (six?) megabytes of the ROM image, setting them all to read/write access.
To help with the latter task, here's some code that will tell you how big the ROM image is:
For the former task, you'll probably need to hit an ARM datasheet. It seems that if one splits up the page table tweaking and the checking into separate programs, doubleclicking on them individually doesn't fail. So that may be a simpler option? Filer_Run the tweaker, then Filer_Run whatever you want to write to ROM, perhaps?
Your comments:
Please note that while I check this page every so often, I am not able to control what users write; therefore I disclaim all liability for unpleasant and/or infringing and/or defamatory material. Undesired content will be removed as soon as it is noticed. By leaving a comment, you agree not to post material that is illegal or in bad taste, and you should be aware that the time and your IP address are both recorded, should it be necessary to find out who you are. Oh, and don't bother trying to inline HTML. I'm not that stupid! ☺ ADDING COMMENTS DOES NOT WORK IF READING TRANSLATED VERSIONS.
You can now follow comment additions with the comment RSS feed. This is distinct from the b.log RSS feed, so you can subscribe to one or both as you wish.
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted.
RIPA notice: No consent is given for interception of page transmission.