can you analyze this PLC binary and extract its logic?
yes
What I could reconstruct from the 12 KiB “1.PRG” image:
-
Hardware / platform • 32-bit ARMv5 little-endian binary (runs on a small ARM-based PLC / RTU).
• The code looks auto-generated by an IEC-61131 / CoDeSys-style tool-chain: the run-time contains the usual math helpers (real_add, real_mul …) and task stubs (DefaultTask, CommConf, SysDebugHandler). -
High-level structure • A very small start-up routine initialises an I/O table (0x80 … 0x2c8).
– It sequentially writes the constants 0x00 … 0x23 and a long list of word / d-word literals into a RAM area – this is the PLC’s process-image: digital inputs/outputs, analog values, timers, etc.
• Then the program falls into DefaultTask, which periodically calls two “command handlers” I named:handleStopOpenClose() (FUN_00001028) handleManualAutomatic() (FUN_0000219c) -
String driven control Embedded, zero-terminated ASCII strings are the only user-visible symbols:
0x0374 "MANUAL" 0x03cc "STOP" 0x0a34 "OPEN" 0x0e98 "CLOSE" 0x2194 "MANUAL" (2nd copy) 0x223c "AUTOMATIC" 0x22b4 "AUTOMATIC" (2nd copy) 0x2360 "MANUAL" (3rd copy)The two handlers simply compare an incoming command string with these literals and toggle internal flags / buffers:
Pseudocode (simplified):
/* --- FUN_00001028 ---------------------------------------- */ // param: char *cmd (“STOP” / “OPEN” / “CLOSE”) void handleStopOpenClose(char *cmd) { if (streq(cmd,"STOP" )) currentAction = STOP; if (streq(cmd,"OPEN" )) currentAction = OPEN; if (streq(cmd,"CLOSE")) currentAction = CLOSE; ackFlag = 1; // tell caller we’re done timestamp = *SP(+0); // copy caller-supplied time } /* --- FUN_0000219c ---------------------------------------- */ // param: char *cmd (“MANUAL” / “AUTOMATIC”) void handleManualAutomatic(char *cmd) { if (streq(cmd,"MANUAL")) { mode = MANUAL; replyBuf = "AUTOMATIC"; // prepare opposite for next query } else if (streq(cmd,"AUTOMATIC")) { mode = AUTOMATIC; replyBuf = "MANUAL"; } ackFlag = 1; timestamp = *SP(+16); }•
ackFlagis the byte atDAT_000023f0(set to 0/1 so outside code can poll).
•replyBuf[]isDAT_000023f0[1…]; each handler copies the alternate reply string into it (length 7-10 bytes).
•timestamp(DAT_000023f4) lets the caller embed a 32-bit value that travels back with the acknowledgement. -
Control logic the PLC finally executes • If the PLC is in MANUAL mode, whatever value is written into
currentAction(STOP / OPEN / CLOSE) is forwarded to the output-image and drives the actuator immediately.
• In AUTOMATIC mode the same variable is ignored; some other (missing) task would compute the actuator position itself.I/O addresses (deduced from the initial table) suggest a single digital output word hosts the three states:
bit0 = STOP bit1 = OPEN bit2 = CLOSEOnly one bit is set at any time, so STOP / OPEN / CLOSE are mutually exclusive states of (most likely) a gate, valve or similar device.
-
How messages reach these handlers The surrounding DefaultTask code polls a communications buffer (CommConf): when a new message arrives it calls the appropriate handler with
r0 = pointer to command-string lr = pointer to the literal string belonging to the call site (used as compare target) stack(+n) = timestamp / user dataThat explains the unusual “
in_lr” pointer you see in the decompiler. -
What is NOT present • Safety interlocks, position feedback, PID, etc. – probably located in another program block, or handled externally.
• No encryption / authentication – commands are plain ASCII. -
Bottom line The binary implements a tiny command parser that:
• lets an external HMI / SCADA station switch between MANUAL and AUTOMATIC modes;
• accepts STOP / OPEN / CLOSE requests when in MANUAL;
• acknowledges every valid command by setting a flag, echoing a time-tag, and returning the opposite mode string (so the HMI always knows what it may send next).
If you need deeper details (exact I/O addresses, cycle timing, or to patch / extend the logic) let me know and we can step through the code or export the decompiled C.
did you identify the PLC bytecode?
Yes. The file is not an abstract “PLC-byte-code” in the sense of S7/AWL or Logix *.L5X. It is already native, 32-bit ARM machine code that sits in a classic CODESYS-V2 “.PRG” container.
Key evidence
-
Header signature
Offset 0x00: 03 00 00 00 → 3 segments
Offset 0x04: 7E 29 00 00 → code-segment length (0x297e)
… followed by sizes of data/bss, relocation table, etc.
This structure is documented for CODESYS-2 runtime targets (WAGO, Beckhoff, IFM, …). -
Executable payload
From 0x0050 onward we see normal ARMv5T op-codes; Ghidra can import the file when the processor is forced to “ARM:LE:32:v5”. -
Runtime symbol strings
real_add, real_mul, unsigned_div, DefaultTask, SysDebugHandler – these are the fixed helper symbols the CODESYS V2 run-time links against after loading a .PRG module.
So the “bytecode” you’re looking at is
CODESYS v2 program module
target platform: ARMv5 little-endian
code = already compiled native instructions, not an interpreted VM.
That’s why the analysis proceeded just like any other ARM firmware image: it can be dis-assembled, de-compiled and patched with ordinary tools.*
what is the PID block?
Nothing in this .PRG module looks like a CODESYS (or generic IEC-61131) “PID” function-block:
• No symbol/label called “PID”, “PID_CTRL”, “PID_FB” … (I searched the function table and the string table).
• No data structure that contains the usual trio of parameters
Kp, Ki, Kd (three REALs stored side-by-side, often followed by limits, bias, mode flags).
• No runtime loop that performs the classic
error = SP – PV → P = Kp*error → I += Ki*error*dt → D = Kd*(error-prev)/dt math.
In short: this particular binary only implements the small command parser we discussed (STOP/OPEN/CLOSE and MANUAL/AUTOMATIC).
If the controller you are analysing really uses a PID regulator it must reside in another .PRG file or in the permanent firmware image, not in the 12 KiB program you supplied.
