Add Week 4 Binary Ninja notebook and OpenOCD/flash host tooling

- WEEK04-BN.md/.pdf: full Binary Ninja dynamic + static lab for 0x0005 and 0x0008
- README: link the Week 4-BN notebook after Week 4a
- debug-server.sh/.ps1: OpenOCD launcher (USE_CORE=0, -rtos none, BP_ADDR)
- flash.sh/.ps1: UF2 flash helpers
- pyproject.toml: ruff config for root host-side scripts
This commit is contained in:
Kevin Thomas committed 2026-10-03 11:33:10 -04:00
1 parent 34f21bcd52
commit 3e52dd0412
11 files changed
+2264 -1095

No files matched your search

-365
View File
@@ -1,365 +0,0 @@
#!/usr/bin/env python3
import sys
import struct
import subprocess
import re
import os
import os.path
import argparse
import json
from time import sleep
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
UF2_MAGIC_END = 0x0AB16F30 # Ditto
INFO_FILE = "/INFO_UF2.TXT"
appstartaddr = 0x2000
familyid = 0x0
def is_uf2(buf):
w = struct.unpack("<II", buf[0:8])
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
def is_hex(buf):
try:
w = buf[0:30].decode("utf-8")
except UnicodeDecodeError:
return False
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
return True
return False
def convert_from_uf2(buf):
global appstartaddr
global familyid
numblocks = len(buf) // 512
curraddr = None
currfamilyid = None
families_found = {}
prev_flag = None
all_flags_same = True
outp = []
for blockno in range(numblocks):
ptr = blockno * 512
block = buf[ptr:ptr + 512]
hd = struct.unpack(b"<IIIIIIII", block[0:32])
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
print("Skipping block at " + ptr + "; bad magic")
continue
if hd[2] & 1:
# NO-flash flag set; skip block
continue
datalen = hd[4]
if datalen > 476:
assert False, "Invalid UF2 data size at " + ptr
newaddr = hd[3]
if (hd[2] & 0x2000) and (currfamilyid == None):
currfamilyid = hd[7]
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
currfamilyid = hd[7]
curraddr = newaddr
if familyid == 0x0 or familyid == hd[7]:
appstartaddr = newaddr
padding = newaddr - curraddr
if padding < 0:
assert False, "Block out of order at " + ptr
if padding > 10*1024*1024:
assert False, "More than 10M of padding needed at " + ptr
if padding % 4 != 0:
assert False, "Non-word padding size at " + ptr
while padding > 0:
padding -= 4
outp.append(b"\x00\x00\x00\x00")
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
outp.append(block[32 : 32 + datalen])
curraddr = newaddr + datalen
if hd[2] & 0x2000:
if hd[7] in families_found.keys():
if families_found[hd[7]] > newaddr:
families_found[hd[7]] = newaddr
else:
families_found[hd[7]] = newaddr
if prev_flag == None:
prev_flag = hd[2]
if prev_flag != hd[2]:
all_flags_same = False
if blockno == (numblocks - 1):
print("--- UF2 File Header Info ---")
families = load_families()
for family_hex in families_found.keys():
family_short_name = ""
for name, value in families.items():
if value == family_hex:
family_short_name = name
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
if all_flags_same:
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
else:
print("Flags were not all the same")
print("----------------------------")
if len(families_found) > 1 and familyid == 0x0:
outp = []
appstartaddr = 0x0
return b"".join(outp)
def convert_to_carray(file_content):
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
for i in range(len(file_content)):
if i % 16 == 0:
outp += "\n"
outp += "0x%02x, " % file_content[i]
outp += "\n};\n"
return bytes(outp, "utf-8")
def convert_to_uf2(file_content):
global familyid
datapadding = b""
while len(datapadding) < 512 - 256 - 32 - 4:
datapadding += b"\x00\x00\x00\x00"
numblocks = (len(file_content) + 255) // 256
outp = []
for blockno in range(numblocks):
ptr = 256 * blockno
chunk = file_content[ptr:ptr + 256]
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack(b"<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
while len(chunk) < 256:
chunk += b"\x00"
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
assert len(block) == 512
outp.append(block)
return b"".join(outp)
class Block:
def __init__(self, addr, default_data=0xFF):
self.addr = addr
self.bytes = bytearray([default_data] * 256)
def encode(self, blockno, numblocks):
global familyid
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack("<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, self.addr, 256, blockno, numblocks, familyid)
hd += self.bytes[0:256]
while len(hd) < 512 - 4:
hd += b"\x00"
hd += struct.pack("<I", UF2_MAGIC_END)
return hd
def convert_from_hex_to_uf2(buf):
global appstartaddr
appstartaddr = None
upper = 0
currblock = None
blocks = []
for line in buf.split('\n'):
if line[0] != ":":
continue
i = 1
rec = []
while i < len(line) - 1:
rec.append(int(line[i:i+2], 16))
i += 2
tp = rec[3]
if tp == 4:
upper = ((rec[4] << 8) | rec[5]) << 16
elif tp == 2:
upper = ((rec[4] << 8) | rec[5]) << 4
elif tp == 1:
break
elif tp == 0:
addr = upper + ((rec[1] << 8) | rec[2])
if appstartaddr == None:
appstartaddr = addr
i = 4
while i < len(rec) - 1:
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
currblock = Block(addr & ~0xff)
blocks.append(currblock)
currblock.bytes[addr & 0xff] = rec[i]
addr += 1
i += 1
numblocks = len(blocks)
resfile = b""
for i in range(0, numblocks):
resfile += blocks[i].encode(i, numblocks)
return resfile
def to_str(b):
return b.decode("utf-8")
def get_drives():
drives = []
if sys.platform == "win32":
r = subprocess.check_output([
"powershell",
"-Command",
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
])
drive = to_str(r).strip()
if drive:
drives.append(drive)
else:
searchpaths = ["/mnt", "/media"]
if sys.platform == "darwin":
searchpaths = ["/Volumes"]
elif sys.platform == "linux":
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
if "SUDO_USER" in os.environ.keys():
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
for rootpath in searchpaths:
if os.path.isdir(rootpath):
for d in os.listdir(rootpath):
if os.path.isdir(os.path.join(rootpath, d)):
drives.append(os.path.join(rootpath, d))
def has_info(d):
try:
return os.path.isfile(d + INFO_FILE)
except:
return False
return list(filter(has_info, drives))
def board_id(path):
with open(path + INFO_FILE, mode='r') as file:
file_content = file.read()
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
def list_drives():
for d in get_drives():
print(d, board_id(d))
def write_file(name, buf):
with open(name, "wb") as f:
f.write(buf)
print("Wrote %d bytes to %s" % (len(buf), name))
def load_families():
# The expectation is that the `uf2families.json` file is in the same
# directory as this script. Make a path that works using `__file__`
# which contains the full path to this script.
filename = "uf2families.json"
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(pathname) as f:
raw_families = json.load(f)
families = {}
for family in raw_families:
families[family["short_name"]] = int(family["id"], 0)
return families
def main():
global appstartaddr, familyid
def error(msg):
print(msg, file=sys.stderr)
sys.exit(1)
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
help='input file (HEX, BIN or UF2)')
parser.add_argument('-b', '--base', dest='base', type=str,
default="0x2000",
help='set base address of application for BIN format (default: 0x2000)')
parser.add_argument('-f', '--family', dest='family', type=str,
default="0x0",
help='specify familyID - number or name (default: 0x0)')
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
parser.add_argument('-d', '--device', dest="device_path",
help='select a device path to flash')
parser.add_argument('-l', '--list', action='store_true',
help='list connected devices')
parser.add_argument('-c', '--convert', action='store_true',
help='do not flash, just convert')
parser.add_argument('-D', '--deploy', action='store_true',
help='just flash, do not convert')
parser.add_argument('-w', '--wait', action='store_true',
help='wait for device to flash')
parser.add_argument('-C', '--carray', action='store_true',
help='convert binary file to a C array, not UF2')
parser.add_argument('-i', '--info', action='store_true',
help='display header information from UF2, do not convert')
args = parser.parse_args()
appstartaddr = int(args.base, 0)
families = load_families()
if args.family.upper() in families:
familyid = families[args.family.upper()]
else:
try:
familyid = int(args.family, 0)
except ValueError:
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
if args.list:
list_drives()
else:
if not args.input:
error("Need input file")
with open(args.input, mode='rb') as f:
inpbuf = f.read()
from_uf2 = is_uf2(inpbuf)
ext = "uf2"
if args.deploy:
outbuf = inpbuf
elif from_uf2 and not args.info:
outbuf = convert_from_uf2(inpbuf)
ext = "bin"
elif from_uf2 and args.info:
outbuf = ""
convert_from_uf2(inpbuf)
elif is_hex(inpbuf):
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
elif args.carray:
outbuf = convert_to_carray(inpbuf)
ext = "h"
else:
outbuf = convert_to_uf2(inpbuf)
if not args.deploy and not args.info:
print("Converted to %s, output size: %d, start address: 0x%x" %
(ext, len(outbuf), appstartaddr))
if args.convert or ext != "uf2":
if args.output == None:
args.output = "flash." + ext
if args.output:
write_file(args.output, outbuf)
if ext == "uf2" and not args.convert and not args.info:
drives = get_drives()
if len(drives) == 0:
if args.wait:
print("Waiting for drive to deploy...")
while len(drives) == 0:
sleep(0.1)
drives = get_drives()
elif not args.output:
error("No drive to deploy.")
for d in drives:
print("Flashing %s (%s)" % (d, board_id(d)))
write_file(d + "/NEW.UF2", outbuf)
if __name__ == "__main__":
main()
-365
View File
@@ -1,365 +0,0 @@
#!/usr/bin/env python3
import sys
import struct
import subprocess
import re
import os
import os.path
import argparse
import json
from time import sleep
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
UF2_MAGIC_END = 0x0AB16F30 # Ditto
INFO_FILE = "/INFO_UF2.TXT"
appstartaddr = 0x2000
familyid = 0x0
def is_uf2(buf):
w = struct.unpack("<II", buf[0:8])
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
def is_hex(buf):
try:
w = buf[0:30].decode("utf-8")
except UnicodeDecodeError:
return False
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
return True
return False
def convert_from_uf2(buf):
global appstartaddr
global familyid
numblocks = len(buf) // 512
curraddr = None
currfamilyid = None
families_found = {}
prev_flag = None
all_flags_same = True
outp = []
for blockno in range(numblocks):
ptr = blockno * 512
block = buf[ptr:ptr + 512]
hd = struct.unpack(b"<IIIIIIII", block[0:32])
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
print("Skipping block at " + ptr + "; bad magic")
continue
if hd[2] & 1:
# NO-flash flag set; skip block
continue
datalen = hd[4]
if datalen > 476:
assert False, "Invalid UF2 data size at " + ptr
newaddr = hd[3]
if (hd[2] & 0x2000) and (currfamilyid == None):
currfamilyid = hd[7]
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
currfamilyid = hd[7]
curraddr = newaddr
if familyid == 0x0 or familyid == hd[7]:
appstartaddr = newaddr
padding = newaddr - curraddr
if padding < 0:
assert False, "Block out of order at " + ptr
if padding > 10*1024*1024:
assert False, "More than 10M of padding needed at " + ptr
if padding % 4 != 0:
assert False, "Non-word padding size at " + ptr
while padding > 0:
padding -= 4
outp.append(b"\x00\x00\x00\x00")
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
outp.append(block[32 : 32 + datalen])
curraddr = newaddr + datalen
if hd[2] & 0x2000:
if hd[7] in families_found.keys():
if families_found[hd[7]] > newaddr:
families_found[hd[7]] = newaddr
else:
families_found[hd[7]] = newaddr
if prev_flag == None:
prev_flag = hd[2]
if prev_flag != hd[2]:
all_flags_same = False
if blockno == (numblocks - 1):
print("--- UF2 File Header Info ---")
families = load_families()
for family_hex in families_found.keys():
family_short_name = ""
for name, value in families.items():
if value == family_hex:
family_short_name = name
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
if all_flags_same:
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
else:
print("Flags were not all the same")
print("----------------------------")
if len(families_found) > 1 and familyid == 0x0:
outp = []
appstartaddr = 0x0
return b"".join(outp)
def convert_to_carray(file_content):
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
for i in range(len(file_content)):
if i % 16 == 0:
outp += "\n"
outp += "0x%02x, " % file_content[i]
outp += "\n};\n"
return bytes(outp, "utf-8")
def convert_to_uf2(file_content):
global familyid
datapadding = b""
while len(datapadding) < 512 - 256 - 32 - 4:
datapadding += b"\x00\x00\x00\x00"
numblocks = (len(file_content) + 255) // 256
outp = []
for blockno in range(numblocks):
ptr = 256 * blockno
chunk = file_content[ptr:ptr + 256]
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack(b"<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
while len(chunk) < 256:
chunk += b"\x00"
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
assert len(block) == 512
outp.append(block)
return b"".join(outp)
class Block:
def __init__(self, addr, default_data=0xFF):
self.addr = addr
self.bytes = bytearray([default_data] * 256)
def encode(self, blockno, numblocks):
global familyid
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack("<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, self.addr, 256, blockno, numblocks, familyid)
hd += self.bytes[0:256]
while len(hd) < 512 - 4:
hd += b"\x00"
hd += struct.pack("<I", UF2_MAGIC_END)
return hd
def convert_from_hex_to_uf2(buf):
global appstartaddr
appstartaddr = None
upper = 0
currblock = None
blocks = []
for line in buf.split('\n'):
if line[0] != ":":
continue
i = 1
rec = []
while i < len(line) - 1:
rec.append(int(line[i:i+2], 16))
i += 2
tp = rec[3]
if tp == 4:
upper = ((rec[4] << 8) | rec[5]) << 16
elif tp == 2:
upper = ((rec[4] << 8) | rec[5]) << 4
elif tp == 1:
break
elif tp == 0:
addr = upper + ((rec[1] << 8) | rec[2])
if appstartaddr == None:
appstartaddr = addr
i = 4
while i < len(rec) - 1:
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
currblock = Block(addr & ~0xff)
blocks.append(currblock)
currblock.bytes[addr & 0xff] = rec[i]
addr += 1
i += 1
numblocks = len(blocks)
resfile = b""
for i in range(0, numblocks):
resfile += blocks[i].encode(i, numblocks)
return resfile
def to_str(b):
return b.decode("utf-8")
def get_drives():
drives = []
if sys.platform == "win32":
r = subprocess.check_output([
"powershell",
"-Command",
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
])
drive = to_str(r).strip()
if drive:
drives.append(drive)
else:
searchpaths = ["/mnt", "/media"]
if sys.platform == "darwin":
searchpaths = ["/Volumes"]
elif sys.platform == "linux":
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
if "SUDO_USER" in os.environ.keys():
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
for rootpath in searchpaths:
if os.path.isdir(rootpath):
for d in os.listdir(rootpath):
if os.path.isdir(os.path.join(rootpath, d)):
drives.append(os.path.join(rootpath, d))
def has_info(d):
try:
return os.path.isfile(d + INFO_FILE)
except:
return False
return list(filter(has_info, drives))
def board_id(path):
with open(path + INFO_FILE, mode='r') as file:
file_content = file.read()
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
def list_drives():
for d in get_drives():
print(d, board_id(d))
def write_file(name, buf):
with open(name, "wb") as f:
f.write(buf)
print("Wrote %d bytes to %s" % (len(buf), name))
def load_families():
# The expectation is that the `uf2families.json` file is in the same
# directory as this script. Make a path that works using `__file__`
# which contains the full path to this script.
filename = "uf2families.json"
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(pathname) as f:
raw_families = json.load(f)
families = {}
for family in raw_families:
families[family["short_name"]] = int(family["id"], 0)
return families
def main():
global appstartaddr, familyid
def error(msg):
print(msg, file=sys.stderr)
sys.exit(1)
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
help='input file (HEX, BIN or UF2)')
parser.add_argument('-b', '--base', dest='base', type=str,
default="0x2000",
help='set base address of application for BIN format (default: 0x2000)')
parser.add_argument('-f', '--family', dest='family', type=str,
default="0x0",
help='specify familyID - number or name (default: 0x0)')
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
parser.add_argument('-d', '--device', dest="device_path",
help='select a device path to flash')
parser.add_argument('-l', '--list', action='store_true',
help='list connected devices')
parser.add_argument('-c', '--convert', action='store_true',
help='do not flash, just convert')
parser.add_argument('-D', '--deploy', action='store_true',
help='just flash, do not convert')
parser.add_argument('-w', '--wait', action='store_true',
help='wait for device to flash')
parser.add_argument('-C', '--carray', action='store_true',
help='convert binary file to a C array, not UF2')
parser.add_argument('-i', '--info', action='store_true',
help='display header information from UF2, do not convert')
args = parser.parse_args()
appstartaddr = int(args.base, 0)
families = load_families()
if args.family.upper() in families:
familyid = families[args.family.upper()]
else:
try:
familyid = int(args.family, 0)
except ValueError:
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
if args.list:
list_drives()
else:
if not args.input:
error("Need input file")
with open(args.input, mode='rb') as f:
inpbuf = f.read()
from_uf2 = is_uf2(inpbuf)
ext = "uf2"
if args.deploy:
outbuf = inpbuf
elif from_uf2 and not args.info:
outbuf = convert_from_uf2(inpbuf)
ext = "bin"
elif from_uf2 and args.info:
outbuf = ""
convert_from_uf2(inpbuf)
elif is_hex(inpbuf):
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
elif args.carray:
outbuf = convert_to_carray(inpbuf)
ext = "h"
else:
outbuf = convert_to_uf2(inpbuf)
if not args.deploy and not args.info:
print("Converted to %s, output size: %d, start address: 0x%x" %
(ext, len(outbuf), appstartaddr))
if args.convert or ext != "uf2":
if args.output == None:
args.output = "flash." + ext
if args.output:
write_file(args.output, outbuf)
if ext == "uf2" and not args.convert and not args.info:
drives = get_drives()
if len(drives) == 0:
if args.wait:
print("Waiting for drive to deploy...")
while len(drives) == 0:
sleep(0.1)
drives = get_drives()
elif not args.output:
error("No drive to deploy.")
for d in drives:
print("Flashing %s (%s)" % (d, board_id(d)))
write_file(d + "/NEW.UF2", outbuf)
if __name__ == "__main__":
main()
-365
View File
@@ -1,365 +0,0 @@
#!/usr/bin/env python3
import sys
import struct
import subprocess
import re
import os
import os.path
import argparse
import json
from time import sleep
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
UF2_MAGIC_END = 0x0AB16F30 # Ditto
INFO_FILE = "/INFO_UF2.TXT"
appstartaddr = 0x2000
familyid = 0x0
def is_uf2(buf):
w = struct.unpack("<II", buf[0:8])
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
def is_hex(buf):
try:
w = buf[0:30].decode("utf-8")
except UnicodeDecodeError:
return False
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
return True
return False
def convert_from_uf2(buf):
global appstartaddr
global familyid
numblocks = len(buf) // 512
curraddr = None
currfamilyid = None
families_found = {}
prev_flag = None
all_flags_same = True
outp = []
for blockno in range(numblocks):
ptr = blockno * 512
block = buf[ptr:ptr + 512]
hd = struct.unpack(b"<IIIIIIII", block[0:32])
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
print("Skipping block at " + ptr + "; bad magic")
continue
if hd[2] & 1:
# NO-flash flag set; skip block
continue
datalen = hd[4]
if datalen > 476:
assert False, "Invalid UF2 data size at " + ptr
newaddr = hd[3]
if (hd[2] & 0x2000) and (currfamilyid == None):
currfamilyid = hd[7]
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
currfamilyid = hd[7]
curraddr = newaddr
if familyid == 0x0 or familyid == hd[7]:
appstartaddr = newaddr
padding = newaddr - curraddr
if padding < 0:
assert False, "Block out of order at " + ptr
if padding > 10*1024*1024:
assert False, "More than 10M of padding needed at " + ptr
if padding % 4 != 0:
assert False, "Non-word padding size at " + ptr
while padding > 0:
padding -= 4
outp.append(b"\x00\x00\x00\x00")
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
outp.append(block[32 : 32 + datalen])
curraddr = newaddr + datalen
if hd[2] & 0x2000:
if hd[7] in families_found.keys():
if families_found[hd[7]] > newaddr:
families_found[hd[7]] = newaddr
else:
families_found[hd[7]] = newaddr
if prev_flag == None:
prev_flag = hd[2]
if prev_flag != hd[2]:
all_flags_same = False
if blockno == (numblocks - 1):
print("--- UF2 File Header Info ---")
families = load_families()
for family_hex in families_found.keys():
family_short_name = ""
for name, value in families.items():
if value == family_hex:
family_short_name = name
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
if all_flags_same:
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
else:
print("Flags were not all the same")
print("----------------------------")
if len(families_found) > 1 and familyid == 0x0:
outp = []
appstartaddr = 0x0
return b"".join(outp)
def convert_to_carray(file_content):
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
for i in range(len(file_content)):
if i % 16 == 0:
outp += "\n"
outp += "0x%02x, " % file_content[i]
outp += "\n};\n"
return bytes(outp, "utf-8")
def convert_to_uf2(file_content):
global familyid
datapadding = b""
while len(datapadding) < 512 - 256 - 32 - 4:
datapadding += b"\x00\x00\x00\x00"
numblocks = (len(file_content) + 255) // 256
outp = []
for blockno in range(numblocks):
ptr = 256 * blockno
chunk = file_content[ptr:ptr + 256]
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack(b"<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
while len(chunk) < 256:
chunk += b"\x00"
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
assert len(block) == 512
outp.append(block)
return b"".join(outp)
class Block:
def __init__(self, addr, default_data=0xFF):
self.addr = addr
self.bytes = bytearray([default_data] * 256)
def encode(self, blockno, numblocks):
global familyid
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack("<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, self.addr, 256, blockno, numblocks, familyid)
hd += self.bytes[0:256]
while len(hd) < 512 - 4:
hd += b"\x00"
hd += struct.pack("<I", UF2_MAGIC_END)
return hd
def convert_from_hex_to_uf2(buf):
global appstartaddr
appstartaddr = None
upper = 0
currblock = None
blocks = []
for line in buf.split('\n'):
if line[0] != ":":
continue
i = 1
rec = []
while i < len(line) - 1:
rec.append(int(line[i:i+2], 16))
i += 2
tp = rec[3]
if tp == 4:
upper = ((rec[4] << 8) | rec[5]) << 16
elif tp == 2:
upper = ((rec[4] << 8) | rec[5]) << 4
elif tp == 1:
break
elif tp == 0:
addr = upper + ((rec[1] << 8) | rec[2])
if appstartaddr == None:
appstartaddr = addr
i = 4
while i < len(rec) - 1:
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
currblock = Block(addr & ~0xff)
blocks.append(currblock)
currblock.bytes[addr & 0xff] = rec[i]
addr += 1
i += 1
numblocks = len(blocks)
resfile = b""
for i in range(0, numblocks):
resfile += blocks[i].encode(i, numblocks)
return resfile
def to_str(b):
return b.decode("utf-8")
def get_drives():
drives = []
if sys.platform == "win32":
r = subprocess.check_output([
"powershell",
"-Command",
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
])
drive = to_str(r).strip()
if drive:
drives.append(drive)
else:
searchpaths = ["/mnt", "/media"]
if sys.platform == "darwin":
searchpaths = ["/Volumes"]
elif sys.platform == "linux":
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
if "SUDO_USER" in os.environ.keys():
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
for rootpath in searchpaths:
if os.path.isdir(rootpath):
for d in os.listdir(rootpath):
if os.path.isdir(os.path.join(rootpath, d)):
drives.append(os.path.join(rootpath, d))
def has_info(d):
try:
return os.path.isfile(d + INFO_FILE)
except:
return False
return list(filter(has_info, drives))
def board_id(path):
with open(path + INFO_FILE, mode='r') as file:
file_content = file.read()
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
def list_drives():
for d in get_drives():
print(d, board_id(d))
def write_file(name, buf):
with open(name, "wb") as f:
f.write(buf)
print("Wrote %d bytes to %s" % (len(buf), name))
def load_families():
# The expectation is that the `uf2families.json` file is in the same
# directory as this script. Make a path that works using `__file__`
# which contains the full path to this script.
filename = "uf2families.json"
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(pathname) as f:
raw_families = json.load(f)
families = {}
for family in raw_families:
families[family["short_name"]] = int(family["id"], 0)
return families
def main():
global appstartaddr, familyid
def error(msg):
print(msg, file=sys.stderr)
sys.exit(1)
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
help='input file (HEX, BIN or UF2)')
parser.add_argument('-b', '--base', dest='base', type=str,
default="0x2000",
help='set base address of application for BIN format (default: 0x2000)')
parser.add_argument('-f', '--family', dest='family', type=str,
default="0x0",
help='specify familyID - number or name (default: 0x0)')
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
parser.add_argument('-d', '--device', dest="device_path",
help='select a device path to flash')
parser.add_argument('-l', '--list', action='store_true',
help='list connected devices')
parser.add_argument('-c', '--convert', action='store_true',
help='do not flash, just convert')
parser.add_argument('-D', '--deploy', action='store_true',
help='just flash, do not convert')
parser.add_argument('-w', '--wait', action='store_true',
help='wait for device to flash')
parser.add_argument('-C', '--carray', action='store_true',
help='convert binary file to a C array, not UF2')
parser.add_argument('-i', '--info', action='store_true',
help='display header information from UF2, do not convert')
args = parser.parse_args()
appstartaddr = int(args.base, 0)
families = load_families()
if args.family.upper() in families:
familyid = families[args.family.upper()]
else:
try:
familyid = int(args.family, 0)
except ValueError:
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
if args.list:
list_drives()
else:
if not args.input:
error("Need input file")
with open(args.input, mode='rb') as f:
inpbuf = f.read()
from_uf2 = is_uf2(inpbuf)
ext = "uf2"
if args.deploy:
outbuf = inpbuf
elif from_uf2 and not args.info:
outbuf = convert_from_uf2(inpbuf)
ext = "bin"
elif from_uf2 and args.info:
outbuf = ""
convert_from_uf2(inpbuf)
elif is_hex(inpbuf):
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
elif args.carray:
outbuf = convert_to_carray(inpbuf)
ext = "h"
else:
outbuf = convert_to_uf2(inpbuf)
if not args.deploy and not args.info:
print("Converted to %s, output size: %d, start address: 0x%x" %
(ext, len(outbuf), appstartaddr))
if args.convert or ext != "uf2":
if args.output == None:
args.output = "flash." + ext
if args.output:
write_file(args.output, outbuf)
if ext == "uf2" and not args.convert and not args.info:
drives = get_drives()
if len(drives) == 0:
if args.wait:
print("Waiting for drive to deploy...")
while len(drives) == 0:
sleep(0.1)
drives = get_drives()
elif not args.output:
error("No drive to deploy.")
for d in drives:
print("Flashing %s (%s)" % (d, board_id(d)))
write_file(d + "/NEW.UF2", outbuf)
if __name__ == "__main__":
main()
+2
View File
@@ -222,6 +222,8 @@ Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Ba
### Week 4a Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04a.md)
### Week 4-BN Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04-BN.md)
### RP2350 SVD [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/rp2350.svd)
### Chapter 5: Intro To Variables
+1393
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+277
View File
@@ -0,0 +1,277 @@
<#
.SYNOPSIS
Start OpenOCD as a live GDB server for the RP2350 via the Pico Debug Probe.
.DESCRIPTION
Starts OpenOCD in the foreground as a long-running GDB server. It exposes
rp2350.dap.core0 on 127.0.0.1:3333 and leaves the core RUNNING, so that a
debugger (Binary Ninja, or plain GDB) can attach to a target that is already
executing and therefore has sane registers.
This script is deliberately NOT flash.ps1. flash.ps1 programs flash and
exits; this one claims the probe and stays up so you can single-step, read
memory, and set breakpoints. Do not run both at once -- exactly one process
may own the debug probe.
The script ends with "reset run" rather than OpenOCD's default halt. This is
the single most important line in the file; see "Why reset run" below.
.PARAMETER OCD
Optional. Directory containing openocd.exe AND its scripts\ directory.
Overrides the PICO_OPENOCD environment variable.
.ENVIRONMENT
PICO_OPENOCD Directory containing openocd.exe AND its scripts\ directory.
Default: $env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev
The path is used for BOTH the executable and the -s scripts
argument. This must be an OpenOCD build matching your machine
architecture (x64).
ADAPTER_SPEED SWD clock in kHz. Default: 24000.
This is a read-mostly debug session, so a fast clock is fine
and makes stepping noticeably smoother. Drop it (10000 or
lower) if the link is flaky or you are using long dupont
wires instead of the probe's own connector.
USE_CORE Which cores to expose to the client. Default: 0 (core0 only).
Accepted values:
0 core0 only <- use this with Binary Ninja
1 core1 only <- rarely useful
SMP both cores as hwthreads <- see "Why USE_CORE=0"
Do not change this to SMP when driving Binary Ninja. The
explanation below is the whole reason this default is 0.
BP_ADDR OPTIONAL address to halt at during the startup run, as 0x
prefixed hex, e.g. 0x10000234 for main. Unset by default,
which is the normal "attach to a running target" behaviour.
Arms a 2-byte hardware execute breakpoint just before the
final "reset run", so the core runs from the vector table
and stops there on its own, with no client attached yet.
See "Why BP_ADDR is one-shot" below for the important
limitation. Arming a length of 2 is mandatory: the Cortex-M33
comparators are halfword-based and reject anything else.
.NOTES
Requirements:
* Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
* OpenOCD with the rp2350 target script installed. See PICO_OPENOCD.
* Exactly one process may own the debug probe.
* A firmware image already programmed into flash (use flash.ps1 first).
* The Debug Probe must use the WinUSB driver. If OpenOCD reports
"unable to open CMSIS-DAP device", install it with Zadig
(https://zadig.akeo.ie/), selecting "Debug Probe (CMSIS-DAP)" -> WinUSB.
Why USE_CORE=0 (core1 must be hidden from the client)
The RP2350 has two Cortex-M33 cores. Core1 does not start on its own:
nothing in a Pico SDK application releases it from reset unless the program
explicitly does so. If you expose it anyway (USE_CORE=SMP), its registers
read back as meaningless reset defaults:
core1: pc 0x000000ec sp 0xf0000000 xpsr 0x09000000 lr 0x00000147
core0: pc 0x1000320c sp 0x20081f38 xpsr 0xa9000000 lr 0x100030ab
Binary Ninja's register widget renders a memory preview for every register,
which means it treats each register value as an ADDRESS and reads it. The
core1 values above are not real addresses. Each read data-aborts, and
OpenOCD responds by tearing down and re-establishing the SWD debug port,
logging a pair of lines per fault:
Error: Failed to read memory at 0xf0000000
Info : SWD DPIDR 0x4c013477
That becomes a self-sustaining loop of roughly 450 fault-and-recover cycles
every two seconds, which makes the session unusable. It looks exactly like a
broken debugger or a broken firmware. It is neither: it is a configuration
mismatch, and hiding core1 removes it completely.
Why reset run (the target must be released, not halted)
On RP2350, halting during reset stops the core at the boot ROM stub BEFORE
the stack pointer is loaded:
xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000 lr: 0xffffffff
Those values are garbage for every core, including core0. Attaching in that
window triggers the identical fault storm described above. Ending this
script with "reset run" means the core starts from the vector table and is
executing normally by the time you attach, so registers read back correct.
Consequence for the client: ATTACH WHILE THE TARGET IS RUNNING. Do not
press Reset/Restart in Binary Ninja -- it performs reset-halt and puts you
back in the garbage window. If you must reset, send "reset run" over the
OpenOCD telnet port (4444) instead. See WEEK04\WEEK04-BN.md.
Why BP_ADDR is one-shot (read this before relying on it)
A breakpoint armed here DOES fire during the startup "reset run" and halts
the core at your address, so the server comes up parked there and your
debugger can simply attach and look at it. That part works.
What you do NOT get is a reusable breakpoint. As soon as any GDB client
connects, OpenOCD unconditionally flushes every breakpoint it is holding:
Info : accepting 'gdb' connection on tcp/3333
Debug: breakpoints.c:328 breakpoint_remove_all_internal():
[rp2350.dap.core0] Delete all breakpoints
So by the time Binary Ninja is up, the comparator is gone -- reading
0xE0002000 shows zeros, not your address. Consequences:
* The startup stop is single-use. You cannot Resume and re-catch the
same address.
* You cannot use BP_ADDR to stop in a loop that is already running,
because the core only passes that point once per reset.
* main (0x10000234) is a good BP_ADDR value precisely because it is
reached exactly once, right after reset.
To arm anything further, or to re-arm main, do it from the OpenOCD command
port AFTER your debugger has connected -- the order is the whole trick:
nc 127.0.0.1 4444
> bp 0x1000023e 2 hw
> reset run
Arming before the client connects does not work (flushed above), and in
plain GDB the equivalent "hbreak" is cleared by "detach" -- both were
verified by reading the FPB comparator registers back.
Related Binary Ninja bug: Binary Ninja's own breakpoints cannot be used on
this target at all. It sends Z0,<addr>,1 -- a 1-byte packet -- and
OpenOCD answers "only breakpoints of two bytes length supported". Both
Toggle Breakpoint (F2) and Add Hardware Breakpoint (F3) fail, at every
address, and the dialog's Size field is disabled so there is no UI way
around it. gdb_breakpoint_override hard does not change this. Hence the
command-port workflow described above.
Why the remaining OpenOCD flags are set
gdb_breakpoint_override hard
Force every client breakpoint onto the Cortex-M33 hardware comparators
(the RP2350 has 8 breakpoints and 4 watchpoints). Without this, a
client may try to write a BKPT instruction into flash at 0x10000000,
which is read-only XIP memory, and the write fails.
gdb_memory_map disable
Stops the client probing the entire 32 MiB flash map on connect. Pure
Raspberry Pi guidance for suppressing spurious "Failed to read memory"
reports during target discovery.
cortex_m reset_config sysresetreq
The Cortex-M33 in the RP2350 has no VECTRESET. Without this, OpenOCD
warns on every reset:
VECTRESET is not supported on this Cortex-M core, using SYSRESETREQ
instead
NOTE: this MUST be the generic "cortex_m" command, not
"rp2350.dap.core1 cortex_m ...". With USE_CORE=0 the core1 target does
not exist, so a core1-scoped command aborts OpenOCD before "init" runs,
and the script exits silently having printed nothing useful.
adapter speed
See ADAPTER_SPEED above.
.EXAMPLE
.\debug-server.ps1
.EXAMPLE
$env:ADAPTER_SPEED=10000; .\debug-server.ps1
.EXAMPLE
$env:PICO_OPENOCD="C:\openocd\bin"; .\debug-server.ps1
.EXAMPLE
$env:BP_ADDR="0x10000234"; .\debug-server.ps1
Comes up halted at main (one-shot; see "Why BP_ADDR is one-shot").
.NOTES
Exit status:
* propagated from OpenOCD. A clean shutdown via Ctrl-C exits 0; an
OpenOCD configuration error exits non-zero.
Related scripts:
flash.ps1 one-shot raw .bin programmer (exits when done)
See also:
WEEK04\WEEK04-BN.md the full walkthrough this script belongs to
#>
[CmdletBinding()]
param(
[Parameter()]
[string]$OCD
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# --- configuration ---------------------------------------------------------
if (-not $OCD) { $OCD = $env:PICO_OPENOCD }
if (-not $OCD) { $OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev" }
$SPEED = if ($env:ADAPTER_SPEED) { $env:ADAPTER_SPEED } else { "24000" }
$USE_CORE = if ($env:USE_CORE) { $env:USE_CORE } else { "0" }
$BP_ADDR = if ($env:BP_ADDR) { $env:BP_ADDR.Trim() } else { "" }
if ($BP_ADDR -and $BP_ADDR -notmatch '^0x[0-9a-fA-F]+$') {
Write-Error "BP_ADDR must be 0x-prefixed hex, e.g. 0x10000234 (got '$BP_ADDR')"
exit 1
}
if (-not (Test-Path "$OCD\openocd.exe")) {
Write-Error "OpenOCD not found at $OCD\openocd.exe (set PICO_OPENOCD or -OCD to the directory containing openocd.exe)"
exit 1
}
# --- announce, so the operator can verify intent before the target is touched --
Write-Host "Starting OpenOCD GDB server on 127.0.0.1:3333 using $OCD\openocd.exe"
Write-Host "SWD adapter speed: $SPEED kHz"
Write-Host "Cores exposed to GDB (USE_CORE): $USE_CORE"
Write-Host "Target will be reset and released (reset run) - attach while it is running."
if ($BP_ADDR) {
Write-Host "Startup breakpoint at $BP_ADDR (2-byte hardware execute, one-shot)."
Write-Host " It fires during this startup reset run. Any client connecting later"
Write-Host " causes OpenOCD to delete it, so arm further breakpoints on port 4444"
Write-Host " after attaching: bp <addr> 2 hw"
}
# --- start -----------------------------------------------------------------
#
# Order matters: USE_CORE must be set BEFORE -f target/rp2350.cfg is read,
# because the target script branches on it when creating the DAP targets.
#
# "reset run" is last so it happens after init and after the target is
# examined, releasing the core rather than halting it. See .NOTES for why.
#
# Any BP_ADDR breakpoint is inserted after "init" (the target must exist before
# a comparator can be programmed) but before "reset run" (so the core is already
# armed when it starts). The length must be 2: Cortex-M33 comparators reject
# other widths.
$ocdArgs = @(
"-s", "$OCD\scripts",
"-f", "interface/cmsis-dap.cfg",
"-c", "set USE_CORE $USE_CORE",
"-f", "target/rp2350.cfg",
# Drop the hwthread RTOS the RP2350 target script attaches to core0.
#
# target/rp2350.cfg creates core0 with "-rtos hwthread", which registers a
# fake RTOS whose "current thread" is coreid+1 = 1. Binary Ninja single-steps
# with the GDB packet "vCont;s" and no thread id, i.e. thread 0. OpenOCD's
# gdb_server sees rtos->current_thread (1) != thread_id (0) and takes its
# "fake step" path, replying with a stop without ever stepping the core:
#
# gdb_server.c gdb_handle_vcont_packet(): fake step thread 0
#
# The result is that Step Into / Step Over in Binary Ninja does nothing: the
# PC never moves. Clearing the RTOS removes the mismatch so the step is real.
# Harmless for single-core use, which is all this lab does (USE_CORE=0).
"-c", "rp2350.dap.core0 configure -rtos none",
"-c", "adapter speed $SPEED",
"-c", "gdb_memory_map disable",
"-c", "gdb_breakpoint_override hard",
"-c", "cortex_m reset_config sysresetreq",
"-c", "init"
)
if ($BP_ADDR) {
$ocdArgs += @("-c", "bp $BP_ADDR 2 hw")
}
$ocdArgs += @("-c", "reset run")
& "$OCD\openocd.exe" @ocdArgs
exit $LASTEXITCODE
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env bash
#
# debug-server.sh - start OpenOCD as a live GDB server for the RP2350 via the Pico Debug Probe.
#
# Synopsis:
# ./debug-server.sh
#
# Description:
# Starts OpenOCD in the foreground as a long-running GDB server. It exposes
# rp2350.dap.core0 on 127.0.0.1:3333 and leaves the core RUNNING, so that a
# debugger (Binary Ninja, or plain GDB) can attach to a target that is already
# executing and therefore has sane registers.
#
# This script is deliberately NOT flash.sh. flash.sh programs flash and exits;
# this one claims the probe and stays up so you can single-step, read memory,
# and set breakpoints. Do not run both at once -- exactly one process may own
# the debug probe.
#
# The script ends with "reset run" rather than OpenOCD's default halt. This is
# the single most important line in the file; see "Why reset run" below.
#
# Requirements:
# - Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
# - OpenOCD with the rp2350 target script installed. See PICO_OPENOCD.
# - Exactly one process may own the debug probe.
# - A firmware image already programmed into flash (use flash.sh first).
#
# Environment variables:
# PICO_OPENOCD Directory containing the openocd binary AND its scripts/
# directory. Default: $HOME/.pico-sdk/openocd/0.12.0+dev
# macOS note: an OpenOCD on PATH is frequently the x86_64
# Homebrew build, which will not run under Rosetta on some
# setups and cannot talk to the ARM64 firmware tooling. The
# Pico SDK ships an arm64 build; point this at it explicitly.
# ADAPTER_SPEED SWD clock in kHz. Default: 24000.
# This is a read-mostly debug session, so a fast clock is fine
# and makes stepping noticeably smoother. Drop it (10000 or
# lower) if the link is flaky or you are using long dupont
# wires instead of the probe's own connector.
# BP_ADDR OPTIONAL address to halt at during the startup run, as 0x
# prefixed hex, e.g. 0x10000234 for main. Unset by default,
# which is the normal "attach to a running target" behavior.
# Arms a 2-byte hardware execute breakpoint just before the
# final "reset run", so the core runs from the vector table
# and stops there on its own, with no client attached yet.
# See "Why BP_ADDR is one-shot" below for the important
# limitation. Arming a length of 2 is mandatory: the Cortex-M33
# comparators are halfword-based and reject anything else.
# USE_CORE Which cores to expose to the client. Default: 0 (core0 only).
# Accepted values:
# 0 core0 only <- use this with Binary Ninja
# 1 core1 only <- rarely useful
# SMP both cores as hwthreads <- see "Why USE_CORE=0"
# Do not change this to SMP when driving Binary Ninja. The
# explanation below is the whole reason this default is 0.
#
# Why USE_CORE=0 (core1 must be hidden from the client)
# The RP2350 has two Cortex-M33 cores. Core1 does not start on its own: nothing
# in a Pico SDK application releases it from reset unless the program
# explicitly does so. If you expose it anyway (USE_CORE=SMP), its registers
# read back as meaningless reset defaults:
#
# core1: pc 0x000000ec sp 0xf0000000 xpsr 0x09000000 lr 0x00000147
# core0: pc 0x1000320c sp 0x20081f38 xpsr 0xa9000000 lr 0x100030ab
#
# Binary Ninja's register widget renders a memory preview for every register,
# which means it treats each register value as an ADDRESS and reads it. The
# core1 values above are not real addresses. Each read data-aborts, and
# OpenOCD responds by tearing down and re-establishing the SWD debug port,
# logging a pair of lines per fault:
#
# Error: Failed to read memory at 0xf0000000
# Info : SWD DPIDR 0x4c013477
#
# That becomes a self-sustaining loop of roughly 450 fault-and-recover cycles
# every two seconds, which makes the session unusable. It looks exactly like a
# broken debugger or a broken firmware. It is neither: it is a configuration
# mismatch, and hiding core1 removes it completely.
#
# Why reset run (the target must be released, not halted)
# On RP2350, halting during reset stops the core at the boot ROM stub BEFORE
# the stack pointer is loaded:
#
# xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000 lr: 0xffffffff
#
# Those values are garbage for every core, including core0. Attaching in that
# window triggers the identical fault storm described above. Ending this
# script with "reset run" means the core starts from the vector table and is
# executing normally by the time you attach, so registers read back correct.
#
# Consequence for the client: ATTACH WHILE THE TARGET IS RUNNING. Do not
# press Reset/Restart in Binary Ninja -- it performs reset-halt and puts you
# back in the garbage window. If you must reset, send "reset run" over the
# OpenOCD telnet port (4444) instead:
#
# nc 127.0.0.1 4444 then type: reset run
#
# ...or use debug-server.sh's restart instructions in WEEK04/WEEK04-BN.md.
#
# Why BP_ADDR is one-shot (read this before relying on it)
# A breakpoint armed here DOES fire during the startup "reset run" and halts
# the core at your address, so the server comes up parked there and your
# debugger can simply attach and look at it. That part works.
#
# What you do NOT get is a reusable breakpoint. As soon as any GDB client
# connects, OpenOCD unconditionally flushes every breakpoint it is holding:
#
# Info : accepting 'gdb' connection on tcp/3333
# Debug: breakpoints.c:328 breakpoint_remove_all_internal():
# [rp2350.dap.core0] Delete all breakpoints
#
# So by the time Binary Ninja is up, the comparator is gone -- reading
# 0xE0002000 shows zeros, not your address. Consequences:
#
# * The startup stop is single-use. You cannot Resume and re-catch the
# same address.
# * You cannot use BP_ADDR to stop in a loop that is already running,
# because the core only passes that point once per reset.
# * main (0x10000234) is a good BP_ADDR value precisely because it is
# reached exactly once, right after reset.
#
# To arm anything further, or to re-arm main, do it from the OpenOCD command
# port AFTER your debugger has connected -- the order is the whole trick:
#
# nc 127.0.0.1 4444
# > bp 0x1000023e 2 hw
# > reset run
#
# Arming before the client connects does not work (flushed above), and in
# plain GDB the equivalent "hbreak" is cleared by "detach" -- both were
# verified by reading the FPB comparator registers back.
#
# Related Binary Ninja bug: Binary Ninja's own breakpoints cannot be used on
# this target at all. It sends Z0,<addr>,1 -- a 1-byte packet -- and OpenOCD
# answers "only breakpoints of two bytes length supported". Both Toggle
# Breakpoint (F2) and Add Hardware Breakpoint (F3) fail, at every address, and
# the dialog's Size field is disabled so there is no UI way around it.
# gdb_breakpoint_override hard does not change this. Hence the command-port
# workflow described above.
#
# Why the remaining OpenOCD flags are set
# gdb_breakpoint_override hard
# Force every client breakpoint onto the Cortex-M33 hardware comparators
# (the RP2350 has 8 breakpoints and 4 watchpoints). Without this, a client
# may try to write a BKPT instruction into flash at 0x10000000, which is
# read-only XIP memory, and the write fails.
# gdb_memory_map disable
# Stops the client probing the entire 32 MiB flash map on connect. Pure
# Raspberry Pi guidance for suppressing spurious "Failed to read memory"
# reports during target discovery.
# cortex_m reset_config sysresetreq
# The Cortex-M33 in the RP2350 has no VECTRESET. Without this, OpenOCD
# warns on every reset:
# VECTRESET is not supported on this Cortex-M core, using SYSRESETREQ
# instead
# NOTE: this MUST be the generic "cortex_m" command, not
# "rp2350.dap.core1 cortex_m ...". With USE_CORE=0 the core1 target does
# not exist, so a core1-scoped command aborts OpenOCD before "init" runs,
# and the script exits silently having printed nothing useful.
# adapter speed
# See ADAPTER_SPEED above.
#
# Examples:
# ./debug-server.sh
# ADAPTER_SPEED=10000 ./debug-server.sh
# PICO_OPENOCD=/opt/homebrew/bin ./debug-server.sh
# BP_ADDR=0x10000234 ./debug-server.sh
# Comes up halted at main (one-shot; see "Why BP_ADDR is one-shot").
#
# Exit status:
# * propagated from OpenOCD. A clean shutdown via Ctrl-C exits 0; an
# OpenOCD configuration error exits non-zero.
#
# Related scripts:
# flash.sh / flash.ps1 one-shot raw .bin programmer (exits when done)
#
# See also:
# WEEK04/WEEK04-BN.md the full walkthrough this script belongs to
set -euo pipefail
# --- configuration ---------------------------------------------------------
OCD="${PICO_OPENOCD:-$HOME/.pico-sdk/openocd/0.12.0+dev}"
SPEED="${ADAPTER_SPEED:-24000}"
USE_CORE="${USE_CORE:-0}"
BP_ADDR="${BP_ADDR:-}"
if [ -n "$BP_ADDR" ] && ! printf '%s' "$BP_ADDR" | grep -qiE '^0x[0-9a-f]+$'; then
echo "error: BP_ADDR must be 0x-prefixed hex, e.g. 0x10000234 (got '$BP_ADDR')" >&2
exit 1
fi
if [ ! -x "$OCD/openocd" ]; then
echo "error: OpenOCD not found at $OCD/openocd" >&2
echo " set PICO_OPENOCD=/path/to/openocd (the directory containing the openocd binary)" >&2
exit 1
fi
# --- announce, so the operator can verify intent before the target is touched --
echo "Starting OpenOCD GDB server on 127.0.0.1:3333 using $OCD/openocd"
echo "SWD adapter speed: ${SPEED} kHz"
echo "Cores exposed to GDB (USE_CORE): ${USE_CORE}"
echo "Target will be reset and released (reset run) - attach while it is running."
if [ -n "$BP_ADDR" ]; then
echo "Startup breakpoint at ${BP_ADDR} (2-byte hardware execute, one-shot)."
echo " It fires during this startup reset run. Any client connecting later"
echo " causes OpenOCD to delete it, so arm further breakpoints on port 4444"
echo " after attaching: bp <addr> 2 hw"
fi
# --- start -----------------------------------------------------------------
#
# Order matters: USE_CORE must be set BEFORE -f target/rp2350.cfg is read,
# because the target script branches on it when creating the DAP targets.
#
# "reset run" is last so it happens after init and after the target is
# examined, releasing the core rather than halting it. See the header for why.
#
# Any BP_ADDR breakpoint is inserted after "init" (the target must exist before
# a comparator can be programmed) but before "reset run" (so the core is already
# armed when it starts). The length must be 2: Cortex-M33 comparators reject
# other widths.
ocd_args=(
-s "$OCD/scripts"
-f interface/cmsis-dap.cfg
-c "set USE_CORE ${USE_CORE}"
-f target/rp2350.cfg
# Drop the hwthread RTOS the RP2350 target script attaches to core0.
#
# target/rp2350.cfg creates core0 with "-rtos hwthread", which registers a
# fake RTOS whose "current thread" is coreid+1 = 1. Binary Ninja single-steps
# with the GDB packet "vCont;s" and no thread id, i.e. thread 0. OpenOCD's
# gdb_server sees rtos->current_thread (1) != thread_id (0) and takes its
# "fake step" path, replying with a stop without ever stepping the core:
#
# gdb_server.c gdb_handle_vcont_packet(): fake step thread 0
#
# The result is that Step Into / Step Over in Binary Ninja does nothing: the
# PC never moves. Clearing the RTOS removes the mismatch so the step is real.
# Harmless for single-core use, which is all this lab does (USE_CORE=0).
-c "rp2350.dap.core0 configure -rtos none"
-c "adapter speed ${SPEED}"
-c "gdb_memory_map disable"
-c "gdb_breakpoint_override hard"
-c "cortex_m reset_config sysresetreq"
-c "init"
)
if [ -n "$BP_ADDR" ]; then
ocd_args+=(-c "bp ${BP_ADDR} 2 hw")
fi
ocd_args+=(-c "reset run")
exec "$OCD/openocd" "${ocd_args[@]}"
+128
View File
@@ -0,0 +1,128 @@
<#
.SYNOPSIS
Program a raw .bin into RP2350 XIP flash via the Pico Debug Probe and OpenOCD.
.DESCRIPTION
Writes a headerless raw binary to the RP2350's external XIP flash starting at
physical address 0x10000000, verifies the written bytes by reading them back,
then releases the core so the freshly programmed firmware runs.
A raw .bin carries no address information, no entry point and no section
table, so the load address MUST be supplied out of band. On the RP2350 the
only correct value is 0x10000000: that is where the boot ROM jumps after
pulling the reset vector out of the on-chip XIP window. Flashing anywhere
else produces a board that enumerates over USB and then does nothing.
The target must already be running the stock RP2350 bootrom (the default
state after power-up, or after any BOOTSEL + UF2 reflash). OpenOCD attaches
to the bootrom over SWD, halts it, programs flash, verifies, and resets.
This is the macOS / Linux equivalent of flash.sh. The two must stay in
lockstep: same base address, same verify, same conservative SWD clock.
.PARAMETER Bin
Mandatory. Path to the raw .bin image to program.
.ENVIRONMENT
PICO_OPENOCD Directory containing openocd.exe AND its scripts\ directory.
Default: $env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev
The path is used for BOTH the executable and the -s scripts
argument.
ADAPTER_SPEED SWD clock in kHz. Default: 5000.
5000 kHz is deliberately conservative. This is a write path,
not a read-only debug session, and a marginal USB cable or
long dupont run produces spurious verify failures at higher
clocks. Raise it (24000) for read-only work; if
"Error: target not halted" or verify mismatches appear,
lower it to 1000.
.EXAMPLE
.\flash.ps1 -Bin 0x0005_intro-to-variables\build\0x0005_intro-to-variables.bin
.EXAMPLE
$env:ADAPTER_SPEED=1000; .\flash.ps1 -Bin build\hacked.bin
.EXAMPLE
$env:PICO_OPENOCD="C:\openocd\bin"; .\flash.ps1 -Bin build\hacked.bin
.NOTES
Requirements:
* Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
* OpenOCD with the rp2350 target script installed.
* The Debug Probe must use the WinUSB driver. If OpenOCD reports
"unable to open CMSIS-DAP device", install it with Zadig
(https://zadig.akeo.ie/), selecting "Debug Probe (CMSIS-DAP)" -> WinUSB.
* Exactly one process may own the debug probe. Close any other OpenOCD,
GDB, or IDE debug session first.
Exit status:
0 flash written, verified, and core released
1 input file missing or OpenOCD not found
* any other status is propagated from OpenOCD, so a failed verify or a
write error is visible to the caller rather than being swallowed.
Related scripts:
debug-server.ps1 long-running GDB server for live debugging with
Binary Ninja. Use that instead of this script when you
need to single-step.
See also:
WEEK04\WEEK04-BN.md the full walkthrough this script belongs to
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Bin
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# --- argument validation ---------------------------------------------------
if (-not (Test-Path -PathType Leaf $Bin)) {
Write-Error "file not found: $Bin"
Write-Host "build it first: cmake -B build -G Ninja -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 ; cmake --build build"
exit 1
}
# --- toolchain resolution --------------------------------------------------
$OCD = if ($env:PICO_OPENOCD) { $env:PICO_OPENOCD } else { "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev" }
if (-not (Test-Path "$OCD\openocd.exe")) {
Write-Error "OpenOCD not found at $OCD\openocd.exe (set PICO_OPENOCD to the directory containing openocd.exe)"
exit 1
}
$SPEED = if ($env:ADAPTER_SPEED) { $env:ADAPTER_SPEED } else { "5000" }
# --- program ---------------------------------------------------------------
# OpenOCD flag notes:
# -f interface/cmsis-dap.cfg the Pico Debug Probe is a CMSIS-DAP v1 device
# -f target/rp2350.cfg RP2350 dual Cortex-M33 + RP2350B0-style DAP;
# sets USE_CORE=SMP by default, which is fine here
# because we never hand uninitialised core1
# registers to a debugger. For live debugging use
# debug-server.ps1, which forces USE_CORE=0.
# -c "program BIN 0x10000000 verify reset exit"
# program the write
# 0x10000000 base address (see .DESCRIPTION)
# verify read back and compare every byte; a mismatch aborts
# reset reset the core so the new image starts at its vectors
# exit release the probe and return to the shell
#
# The adapter speed is deliberately lower here than in debug-server.ps1; see
# ADAPTER_SPEED in .ENVIRONMENT.
Write-Host "Flashing $Bin -> 0x10000000 using $OCD\openocd.exe (SWD $SPEED kHz)"
& "$OCD\openocd.exe" `
-s "$OCD\scripts" `
-f interface/cmsis-dap.cfg `
-f target/rp2350.cfg `
-c "adapter speed $SPEED" `
-c "program $Bin 0x10000000 verify reset exit"
exit $LASTEXITCODE
Executable
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
#
# flash.sh - program a raw .bin into RP2350 XIP flash via the Pico Debug Probe + OpenOCD.
#
# Synopsis:
# ./flash.sh <path-to-file.bin>
#
# Description:
# Writes a headerless raw binary to the RP2350's external XIP flash starting at
# physical address 0x10000000, then verifies the written bytes by reading them
# back, then releases the core so the freshly programmed firmware runs.
#
# A raw .bin carries no address information, no entry point and no section
# table, so the load address MUST be supplied out of band. On the RP2350 the
# only correct value is 0x10000000: that is where the boot ROM jumps after
# pulling the reset vector out of the on-chip XIP window. Flashing anywhere
# else produces a board that enumerates over USB and then does nothing.
#
# The target must already be running the stock RP2350 bootrom (the default
# state after power-up or after any BOOTSEL+UF2 reflash). OpenOCD attaches to
# the bootrom via SWD, halts it, programs flash, verifies, and resets.
#
# Requirements:
# - Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
# - OpenOCD with the rp2350 target script installed. The Pico SDK ships one;
# see PICO_OPENOCD below.
# - Exactly one process may own the debug probe. Close any other OpenOCD,
# GDB, or IDE debug session first.
#
# Environment variables:
# PICO_OPENOCD Directory containing the openocd binary AND its scripts/
# directory. Default: $HOME/.pico-sdk/openocd/0.12.0+dev
# The path is used for BOTH the executable and -s scripts.
# ADAPTER_SPEED SWD clock in kHz. Default: 5000.
# 5000 kHz is deliberately conservative. This is a write path,
# not a read-only debug session, and a marginal USB cable or
# long dupont run will produce spurious verify failures at
# higher clocks. Raise it (24000) for read-only work; if
# "Error: target not halted" or verify mismatches appear,
# lower it to 1000.
#
# Examples:
# ./flash.sh 0x0005_intro-to-variables/build/0x0005_intro-to-variables.bin
# ADAPTER_SPEED=1000 ./flash.sh build/hacked.bin
# PICO_OPENOCD=/opt/homebrew/bin ./flash.sh build/hacked.bin
#
# Exit status:
# 0 flash written, verified, and core released
# 1 bad usage, missing input file, or OpenOCD not found
# * any other status is propagated from OpenOCD, so a failed verify or a
# write error is visible to the caller rather than being swallowed.
#
# Related scripts:
# debug-server.sh / debug-server.ps1 long-running GDB server for live
# debugging with Binary Ninja
#
# See also:
# WEEK04/WEEK04-BN.md the full walkthrough this script belongs to
set -euo pipefail
# --- argument validation ---------------------------------------------------
BIN="${1:-}"
if [ -z "$BIN" ]; then
echo "usage: $0 <path-to-file.bin>" >&2
exit 1
fi
if [ ! -f "$BIN" ]; then
echo "error: file not found: $BIN" >&2
echo " build it first: cmake -B build -G Ninja -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 && cmake --build build" >&2
exit 1
fi
# --- toolchain resolution --------------------------------------------------
OCD="${PICO_OPENOCD:-$HOME/.pico-sdk/openocd/0.12.0+dev}"
if [ ! -x "$OCD/openocd" ]; then
echo "error: OpenOCD not found at $OCD/openocd" >&2
echo " set PICO_OPENOCD=/path/to/openocd (the directory containing the openocd binary)" >&2
exit 1
fi
SPEED="${ADAPTER_SPEED:-5000}"
# --- program ---------------------------------------------------------------
# OpenOCD flag notes:
# -f interface/cmsis-dap.cfg the Pico Debug Probe is a CMSIS-DAP v1 device
# -f target/rp2350.cfg RP2350 dual Cortex-M33 + RP2350B0-style DAP;
# sets USE_CORE=SMP by default, which is fine here
# because we never hand uninitialised core1
# registers to a debugger. For live debugging use
# debug-server.sh, which forces USE_CORE=0.
# -c "program BIN 0x10000000 verify reset exit"
# program the write
# 0x10000000 base address (see header)
# verify read back and compare every byte; a mismatch aborts
# reset reset the core so the new image starts at its vectors
# exit release the probe and return to the shell
#
# The adapter speed is deliberately lower here than in debug-server.sh; see
# ADAPTER_SPEED in the header.
echo "Flashing $BIN -> 0x10000000 using $OCD/openocd (SWD ${SPEED} kHz)"
"$OCD/openocd" \
-s "$OCD/scripts" \
-f interface/cmsis-dap.cfg \
-f target/rp2350.cfg \
-c "adapter speed ${SPEED}" \
-c "program $BIN 0x10000000 verify reset exit"
+94
View File
@@ -0,0 +1,94 @@
# Python standards for this repository.
#
# Scope note: only the small number of first-party helper scripts at the repo
# root are linted. Firmware projects under 0x*/ are C/C++/ASM and are not
# Python, and vendored trees (pico-sdk, build/) are excluded explicitly.
#
# Run:
# ruff check .
# ruff format .
# ruff check --fix .
[project]
name = "embedded-hacking-tools"
version = "1.0.0"
description = "Host-side tooling for the Embedded Hacking course"
requires-python = ">=3.10"
[tool.ruff]
target-version = "py310"
line-length = 88
# Scope: the first-party host-side helpers at the repo root only. The lesson
# trees below are per-lesson material that predates this config and are
# intentionally not linted; migrate a file to the repo root, or run ruff on it
# directly with an explicit path, when you touch it.
extend-exclude = [
"0x*",
"WEEK*",
"**/build",
"pico-sdk",
"**/pico-sdk",
"node_modules",
]
[tool.ruff.lint]
# E/W pycodestyle - formatting-independent style
# F pyflakes - unused imports, undefined names, f-string gaps
# I isort - import ordering
# UP pyupgrade - modern syntax for the declared target version
# B bugbear - real bug patterns
# SIM simplify - redundant code
# C4 comprehensions - needless comprehension / generator misuse
# RET return - inconsistent returns
# PTH pathlib - use pathlib instead of os.path
# ARG unused-arguments - dead parameters
# D pydocstyle - docstring presence and convention
# ANN annotations - type annotations
# TRY tryceratops - correct exception handling
# PIE misc - misc lints
# RUF ruff-specific - modern idioms and correctness rules
select = [
"E",
"F",
"I",
"UP",
"B",
"SIM",
"C4",
"RET",
"PTH",
"ARG",
"D",
"ANN",
"TRY",
"PIE",
"RUF",
]
ignore = [
# The UF2 block format is a byte-level binary format; several conversions
# legitimately deal in loose byte slices and magic integers.
"E501",
# C901 complexity: convert_from_uf2 is a linear decoder over a block stream
# and reads far better as one pass than split into helpers.
"C901",
# Legacy CLI scripts print progress to stdout as their primary output.
"T201",
]
[tool.ruff.lint.per-file-ignores]
# Tutorial scripts are run directly and use a permissive argparse style.
"uf2conv.py" = ["D100"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-annotations]
# Accept `-> None` omission only on __init__; require annotations elsewhere.
suppress-dummy-args = false
mypy-init-return = true
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "lf"