Ghidra decompiler - Handling PIC16 self-modifying code and font bitmaps - Page 1
Forum section: electronics / general

author: microvia
author_rank: Regular Contributor
timestamp: August 17, 2025, 03:00:58 pm
content: Hello,I want to analyze the HEX of a PIC16F628 publihed athttps://www.romanblack.com/onesec/CapMeter.htm, which I'm attaching here.The PIC directly drives a 16x2 LCD display without I2C, so contains its own font.What I want to understand is how the C measurement is implemented even if the method is explained on the site and is not much complicated to code. In other words, it's essentialy by curiosity, and also to familiarize myself with a good decompiler.So I tried using Ghidra for the first time yesterday, and found that the code starts with a jump to an address where a whole series of bytes is being written, either to form some hidden code to fool disassemblers, or to initiate data. Several similar sections exist, which I want to understand.Questions:- How can these successive byte writes be analysed to determine if the portion of RAM they occupy is actual code ? Can this portion be added to the map (in a script ?) to avoid irrelevant disassembly like jumps or calls that the decompiler can't understand without code present at the jump addresses ?- Is there a way to show the whole memory map as a bitmap to visully detect graphics such as fonts ? If so, how can the section be defined as data to avoid being analyzed as code ?Thanks.

author: NorthGuy
author_rank: Super Contributor
timestamp: August 17, 2025, 03:06:32 pm
content: Quote from: microvia on August 17, 2025, 03:00:58 pmC compiler must initialize all the variables before it starts executing the program. I bet that's what it is.

author: hans
author_rank: Super Contributor
timestamp: August 17, 2025, 04:44:33 pm
content: Well first by logic; the PIC16 is a Harvard machine architecture. It has a separate memory space for code and data. This means it cant run self-modifying code.In contrast, more recent CPU architectures have some kind of modified Harvard architecture which borrows the single memory space from Von Neumann, but still implements multiple memories for sections of peripheral, FLASH or RAM.With that being said, I agree with NorthGuy.. its likely preinitializing variable values which is part of the regular C runtime before jumping to main().However, assuming this was a different architecture that could execute self-modifying code, I think the best approach is to find a branch instruction that jumps to a value in RAM. If that cannot be found, its unlikely to be self-modifying code. Attaching a debugger to a running process and debugging it on this level of assembly can also be useful (Everything is open source if you can work in assembly).You could also isolate the written bytes and see if decoding them into opcodes makes any sense at all.Reverse engineering and identifying sections of code/data is usually a lot of educated guesses and finding anchor points to reason from. This could include peripheral register addresses, ASCII strings, certain routines (like CMSIS HALs), etc. Again, a debugger could be useful here because it can give some call stack information too.In my experience, reverse engineering is a bit easier on desktop since a desktop program leaves behind better annotated anchor points (like more strings in general, but also sometimes leftover from assertion failures, or library imports/exports). But it can still be a huge time sink.

author: Ian.M
author_rank: Super Contributor
timestamp: August 17, 2025, 08:03:36 pm
content: 8bit PICs in general, including all PIC16 cannot execute from RAM. Exception: *Some* high pin count PIC18 can execute from external parallel memory, and if you use external RAM,you can even write self-modifying code. See:https://ww1.microchip.com/downloads/en/AppNotes/00869b.pdf

author: John Coloccia
author_rank: Super Contributor
timestamp: August 17, 2025, 08:18:15 pm
content: Quote from: microvia on August 17, 2025, 03:00:58 pmGenerally speaking, this following happens when you start up a micro:- the reset vector takes you to the C startup code- the C startup code- initializes the stack pointer- does a bit of other initialization- copy everything out of the data section of NVM into the data section of RAM- zeros everything in RAM that should be zeroes (everything in the .bss section, for example)- jumps to mainThat's a really basic sequence, more or less complicated on various platforms, but they all rhyme. You can write your own startup code, but mostly nobody does (although it's not super uncommon to modify it for various reasons)Incidentally, this is also why you shouldn't initialize global/static variables if you intend them to be zero. They're zero already. Setting them to zero potentially tosses them into data, and they take up space.

author: abyrvalg
author_rank: Frequent Contributor
timestamp: August 18, 2025, 08:43:31 am
content: @microvia, you mean those movlw+movwf at 0x03F9-0x0469? Check the byte values being moved - they are ASCII, that code initializes text strings in RAM: "Cap", "Small", "pF", " Zero Cal ", "AutCal", " ", "Large", "nF", "uF", "Error".The hexdoesn'tcontain any font, it uses atextLCD. The function at 0x02B1 sends ASCII strings to LCD as is.

author: voltsandjolts
author_rank: Unknown
timestamp: August 18, 2025, 09:16:51 am
content: Where's the decomplied C code that ghidra spits out?

author: abyrvalg
author_rank: Frequent Contributor
timestamp: August 18, 2025, 09:49:34 am
content: Quote from: voltsandjolts on August 18, 2025, 09:16:51 amI've used IDA instead of Ghidra, so no decompilation, just asm. Decompiling a PIC16 code to C would result in more mess I'm afraid.

author: microvia
author_rank: Regular Contributor
timestamp: August 18, 2025, 02:42:57 pm
content: Thanks a lot for your interesting replies.I didn't find the ASCII strings as I have no dev environment on the mac I'm using (I'm out for some time).Why are the strings hard-copied from code rather than already stored where they can be pointed to (code section) ?

author: abyrvalg
author_rank: Frequent Contributor
timestamp: August 18, 2025, 05:55:10 pm
content: Harvard architecture again, the code memory can’t be read, only executed. There is a trick to overcome that limitation (an array of retlw xx instructions prepended by an indirect jump), but you’ll need to have a dedicated print function capable of accessing such arrays. And, since the code does prints from RAM (the converted capacitance value), the author have decided to use the same function for strings too, so they must be in RAM.

author: John Coloccia
author_rank: Super Contributor
timestamp: August 19, 2025, 07:17:35 am
content: To get them to stay in flash, you normally make a section and put all the strings (or whatever) in that section. If have enough ram, then this is just extra work so you don't bother.So right now they are probably just being copied along with every other initialized variable. Please look at the basic startup sequence I posted.

author: Adrian_Taylor333
author_rank: Regular Contributor
timestamp: August 19, 2025, 07:47:26 am
content: Quote from: Ian.M on August 17, 2025, 08:03:36 pmA nitpick: true - but strictly speaking, that doesn't prevent PIC16 from modifying its own code. One can erase and reprogram any line in FLASH, provided it doesn't contain currently executed code.

author: Ian.M
author_rank: Super Contributor
timestamp: August 19, 2025, 08:05:06 am
content: Another nitpick: Certainly you can write self-modifying code running from internal FLASH program memory on any PIC that can write to FLASH at runtime, but the O.P's PIC16F628 is from the generation before that, and cant!

author: abyrvalg
author_rank: Frequent Contributor
timestamp: August 19, 2025, 12:53:29 pm
content: Quote from: John Coloccia on August 19, 2025, 07:17:35 amSurprise, you can't do this on a PIC16. This CPU has no instructions to read code memory at all.

author: Adrian_Taylor333
author_rank: Regular Contributor
timestamp: August 19, 2025, 03:50:11 pm
content: Quote from: abyrvalg on August 19, 2025, 12:53:29 pmNot true. PIC16F152/6/7,1947 an many others can access the FLASH easily in many ways.* RETLW instructions* through either INDF0 or INDF1. Code memory starts at 0x8000 IIRC.* through the SFRs for FLASH access ( read/erase/program).IIRC the first method can get you only lowest 8 bits per 14-bit word, while the last two can recover both - 6 bits of the MSB byte and 8 bits of the LSB byte that follows it.

author: Sacodepatatas
author_rank: Regular Contributor
timestamp: August 19, 2025, 05:03:16 pm
content: Quote from: microvia on August 17, 2025, 03:00:58 pmHi, about 15 years back I built an earlier capacitance/inductance meter using the same MCU. I'm surprised that Roman programmed it's design from scratch, instead of leveraging the original code and libraries... The working principle is the same, an oscillator made of a LC tank and a comparator (an RC if using the Roman's design). The original was made in 2004 using a 16F84 and an LM311 as the external comparator. Later a second version got rid of the LM311. The original code was written in assembler, and the ASCII strings were stored in the internal EEPROM.https://sites.google.com/site/vk3bhr/home/index2-html

author: John Coloccia
author_rank: Super Contributor
timestamp: August 20, 2025, 03:52:16 am
content: Quote from: abyrvalg on August 19, 2025, 12:53:29 pmThen there is no way to ever get initialized variables into RAM. This isn't what Harvard architecture means or what the point of it was. These are almost antiquated terms these days.

author: JPortici
author_rank: Super Contributor
timestamp: August 20, 2025, 07:32:21 am
content: Quote from: John Coloccia on August 20, 2025, 03:52:16 amThis MCU does not have the TBLRD instruction so it can't read program flash.However, it has the RETLW instruction which is what is used to load constants in RAM.INDF doesn't apply here, it can't read FLASH on this MCU.

author: Adrian_Taylor333
author_rank: Regular Contributor
timestamp: August 20, 2025, 09:10:17 am
content: Quote from: JPortici on August 20, 2025, 07:32:21 amThis doesn't make sense. RETLW LITERALLY means "read the byte from the very same program word that is being executed ad return it".So, the thing CAN access its own program memory.Being braindead of the braindead world, it doesn't have an CALLW instruction, but that can be emulated trough several steps.

author: JPortici
author_rank: Super Contributor
timestamp: August 20, 2025, 09:59:08 am
content: Quote from: Adrian_Taylor333 on August 20, 2025, 09:10:17 amIt makes sense if we are being pedantic.PIC16 program memory is 14bit wide.RETLW loads the lower 8bit of the program word into W, which is not the same as reading the program memory, but because the PIC used by OP does not have TBLRD there is no way to read the upper 6 bits. The PIC can't read its own flash.

author: Ian.M
author_rank: Super Contributor
timestamp: August 20, 2025, 10:48:35 am
content: Lets avoid some confusion here. PIC16 part numbers include *THREE* different families of MCU cores, the Baseline (12 bit) core, and the Midrange and Enhanced Midrange (14 bit) cores. Only the Enhanced Midrange core can access FLASH using INDF. 'Classic' Midrange core PIC16 *with* runtime FLASH read/write support use a mechanism similar to that for EEPROM access to read/write FLASH, using mostly the same SFRs.The O.P's PIC16F628 is a 'classic' Midrange 14 bit core part without runtime FLASH access.8 bit PIC part numbers have been a mess for a long time. See my post over on the Microchip forums back in 2011:https://forum.microchip.com/s/topic/a5C3l000000ML9nEAG/t311387(formatting FUBARed by forum migration)Nowadays to complicate things even further there are Enhanced Midrange PICs with five digits after the separator, and they've dropped the distinction that 'PIC12' means an eight pin part for these 'five digit' parts and made them all 'PIC16nnnnn'.

author: Adrian_Taylor333
author_rank: Regular Contributor
timestamp: August 20, 2025, 11:09:03 am
content: Quote from: JPortici on August 20, 2025, 09:59:08 amRETLW literally reads teh program memory. Just not all of it.In any case, even this braindead² PIC has all it needs to initialize RAM from the program memory.Funny thing: ever since we got into geopolitics of electronics, my browser behaves weird after every visit of the site.Human swarms are everywhere.What happens when GLADIO networks crash down around the globe?Does CIA guarantee health insurance to all of their drones indefinitely ? Or at all ? 🤣

author: Sacodepatatas
author_rank: Regular Contributor
timestamp: August 20, 2025, 12:30:24 pm
content: Quote from: John Coloccia on August 20, 2025, 03:52:16 amFollowing with that logic, a brainfuck machine can also access data from code...Harvard architecture refers to the manner data and instruction buses point to different memory spaces, but it doesn't mean that each instruction doesn't have an implicit data field that can be transferred to another memory scope.

author: John Coloccia
author_rank: Super Contributor
timestamp: August 20, 2025, 11:01:34 pm
content: My vague memory is you can copy data nto ram just fine, but you do have to get it into ram before you can actually use the value. Maybe I'm confusing pics with something else.I'm not sure I've seen a chip that literally can't do anything with rom other than execute instructions. Perhaps I've blocked them from my memory.

author: Ian.M
author_rank: Super Contributor
timestamp: August 21, 2025, 05:51:03 am
content: Quote from: John Coloccia on August 20, 2025, 11:01:34 pmYou've seen them and been sufficiently traumatised to block the memory. The old PIC12/PIC16 Baseline (12 bit) core and 'classic' Midrange 14 bit core without runtime FLASH access cant access their program memory. Yes, you can write a subroutine to perform a computed jump (by overwriting the program counter) into a table of RETLW instructions, (which each do two things: load an 8 bit literal into W, and return from subroutine, but don't actually perform a memory access) so you can effectively read constants from program memory, but make the slightest mistake and compute a jump that doesn't land on a RETLW and execution goes off in La-La land running whatever code it landed on instead of giving you back data in W. The details of computing a jump to an arbitrary address are gnarly, with page boundaries to consider.

author: westfw
author_rank: Super Contributor
timestamp: August 21, 2025, 08:44:11 am
content: QuoteOh come on. RETLW is no more "reading the flash" than "MOVLW"it's an instruction (we know that the PIC can read instructions from flash, right?) that loads an immediate value into the AC (and returns from a "call.")On PIC16s, it's frequently used to return "strings" or tables (one byte (the Nth) at a time) in conjunction with adding N to the PC in a subroutine, but it's not ... very nice.For setting up data memory from constants, a bunch of MOVLW/MOVWF instructions is equivalent.

author: John Coloccia
author_rank: Super Contributor
timestamp: August 21, 2025, 04:05:54 pm
content: Is that how they init the ram? It sounds familiar, and yes I've certainly blocked it. Lol.