1-- license:BSD-3-Clause
2-- copyright-holders: Carl
3local exports = {}
4exports.name = "gdbstub"
5exports.version = "0.0.1"
6exports.description = "GDB stub plugin"
7exports.license = "The BSD 3-Clause License"
8exports.author = { name = "Carl" }
9
10local gdbstub = exports
11
12-- percpu mapping of mame registers to gdb register order
13local regmaps = {
14	i386 = {
15		togdb = {
16			EAX = 1, ECX = 2, EDX = 3, EBX = 4, ESP = 5, EBP = 6, ESI = 7, EDI = 8, EIP = 9, EFLAGS = 10, CS = 11, SS = 12,
17			DS = 13, ES = 14, FS = 15, GS = 16 },
18		fromgdb = {
19			"EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI", "EIP", "EFLAGS", "CS", "SS", "DS", "ES", "FS", "GS" },
20		regsize = 4,
21		addrsize = 4,
22		pcreg = "EIP"
23	}
24}
25regmaps.i486 = regmaps.i386
26regmaps.pentium = regmaps.i386
27
28function gdbstub.startplugin()
29	local debugger
30	local debug
31	local cpu
32	local breaks
33	local watches
34	local consolelog
35	local consolelast
36	local running
37
38	emu.register_start(function ()
39		debugger = manager:machine():debugger()
40		if not debugger then
41			print("gdbstub: debugger not enabled")
42			return
43		end
44		cpu = manager:machine().devices[":maincpu"]
45		if not cpu then
46			print("gdbstub: maincpu not found")
47		end
48		if not regmaps[cpu:shortname()] then
49			print("gdbstub: no register map for cpu " .. cpu:shortname())
50			cpu = nil
51		end
52		consolelog = debugger.consolelog
53		consolelast = 0
54		breaks = {byaddr = {}, byidx = {}}
55		watches = {byaddr = {}, byidx = {}}
56		running = false
57	end)
58
59	emu.register_stop(function()
60		consolelog = nil
61		cpu = nil
62		debug = nil
63	end)
64
65	local socket = emu.file("", 7)
66	local connected = false
67	socket:open("socket.127.0.0.1:2159")
68
69	emu.register_periodic(function ()
70		if not cpu then
71			return
72		end
73
74		if running and debugger.execution_state == "stop" then
75			socket:write("$S05#B8")
76			running = false
77			return
78		elseif debugger.execution_state == "run" then
79			running = true
80		end
81
82		local function chksum(str)
83			local sum = 0
84			str:gsub(".", function(s) sum = sum + s:byte() end)
85			return string.format("%.2x", sum & 0xff)
86		end
87
88		local function makebestr(val, len)
89			local str = ""
90			for count = 0, len - 1 do
91				str = str .. string.format("%.2x", (val >> (count * 8)) & 0xff)
92			end
93			return str
94		end
95
96		local last = consolelast
97		local msg = consolelog[#consolelog]
98		consolelast = #consolelog
99		if #consolelog > last and msg:find("Stopped at", 1, true) then
100			local point = tonumber(msg:match("Stopped at breakpoint ([0-9]+)"))
101			local map = regmaps[cpu:shortname()]
102			running = false
103			if not point then
104				point = tonumber(msg:match("Stopped at watchpoint ([0-9]+"))
105				if not point then
106					return -- ??
107				end
108				local wp = watches.byidx[point]
109				if wp then
110					local reply = "T05" .. wp.type .. ":" .. makebestr(wp.addr, map.addrsize)
111					socket:write("$" .. reply .. "#" .. chksum(reply))
112				else
113					socket:write("$S05#B8")
114				end
115				return
116			else
117				local bp = breaks.byidx[point]
118				if bp then
119					local reply = "T05hwbreak:" .. makebestr(cpu.state[map.pcreg].value, map.regsize)
120					socket:write("$" .. reply .. "#" .. chksum(reply))
121				else
122					socket:write("$S05#B8")
123				end
124				return
125			end
126		end
127
128		if running and debugger.execution_state == "stop" then
129			socket:write("$S05#B8")
130			running = false
131			return
132		elseif debugger.execution_state == "run" then
133			running = true
134		end
135
136		local data = ""
137
138		repeat
139			local read = socket:read(100)
140			data = data .. read
141		until #read == 0
142		if #data == 0 then
143			return
144		end
145		if data == "\x03" then
146			debugger.execution_state = "stop"
147			socket:write("$S05#B8")
148			running = false
149			return
150		end
151		local packet, checksum = data:match("%$([^#]+)#(%x%x)")
152		if packet then
153			packet:gsub("}(.)", function(s) return string.char(string.byte(s) ~ 0x20) end)
154			local cmd = packet:sub(1, 1)
155			local map = regmaps[cpu:shortname()]
156			if cmd == "g" then
157				local regs = {}
158				for reg, idx in pairs(map.togdb) do
159					regs[idx] = makebestr(cpu.state[reg].value, map.regsize)
160				end
161				local data = table.concat(regs)
162				socket:write("+$" .. data .. "#" .. chksum(data))
163			elseif cmd == "G" then
164				local count = 0
165				packet:sub(2):gsub(string.rep("%x", map.regsize * 2), function(s)
166						count = count + 1
167						cpu.state[map.fromgdb[count]].value = tonumber(s,16)
168					end)
169				socket:write("+$OK#9a")
170			elseif cmd == "m" then
171				local addr, len = packet:match("m(%x+),(%x+)")
172				if addr and len then
173					addr = tonumber(addr, 16)
174					len = tonumber(len, 16)
175					local data = ""
176					local space = cpu.spaces["program"]
177					for count = 1, len do
178						data = data .. string.format("%.2x", space:read_log_u8(addr))
179						addr = addr + 1
180					end
181					socket:write("+$" .. data .. "#" .. chksum(data))
182				else
183					socket:write("+$E00#a5") -- fix error
184				end
185			elseif cmd == "M" then
186				local count = 0
187				local addr, len, data = packet:match("M(%x+),(%x+),(%x+)")
188				if addr and len and data then
189					addr = tonumber(addr, 16)
190					local space = cpu.spaces["program"]
191					data:gsub("%x%x", function(s) space:write_log_u8(addr + count, tonumber(s, 16)) count = count + 1 end)
192					socket:write("+$OK#9a")
193				else
194					socket:write("+$E00#a5")
195				end
196			elseif cmd == "s" then
197				if #packet == 1 then
198					cpu:debug():step()
199					socket:write("+$OK#9a")
200					socket:write("$S05#B8")
201					running = false
202				else
203					socket:write("+$E00#a5")
204				end
205			elseif cmd == "c" then
206				if #packet == 1 then
207					cpu:debug():go()
208					socket:write("+$OK#9a")
209				else
210					socket:write("+$E00#a5")
211				end
212			elseif cmd == "Z" then
213				local btype, addr, kind = packet:match("Z([0-4]),(%x+),(.*)")
214				addr = tonumber(addr, 16)
215				if btype == "0" then
216					socket:write("") -- is machine dependant
217				elseif btype == "1" then
218					if breaks.byaddr[addr] then
219						socket:write("+$E00#a5")
220						return
221					end
222					local idx = cpu:debug():bpset(addr)
223					breaks.byaddr[addr] = idx
224					breaks.byidx[idx] = addr
225					socket:write("+$OK#9a")
226				elseif btype == "2" then
227					if watches.byaddr[addr] then
228						socket:write("+$E00#a5")
229						return
230					end
231					local idx = cpu:debug():wpset(cpu.spaces["program"], "w", addr, 1)
232					watches.byaddr[addr] = idx
233					watches.byidx[idx] = {addr = addr, type = "watch"}
234					socket:write("+$OK#9a")
235				elseif btype == "3" then
236					if watches.byaddr[addr] then
237						socket:write("+$E00#a5")
238						return
239					end
240					local idx = cpu:debug():wpset(cpu.spaces["program"], "r", addr, 1)
241					watches.byaddr[addr] = idx
242					watches.byidx[idx] = {addr = addr, type = "rwatch"}
243					socket:write("+$OK#9a")
244				elseif btype == "4" then
245					if watches.byaddr[addr] then
246						socket:write("+$E00#a5")
247						return
248					end
249					local idx = cpu:debug():wpset(cpu.spaces["program"], "rw", addr, 1)
250					watches.byaddr[addr] = idx
251					watches.byidx[idx] = {addr = addr, type = "awatch"}
252					socket:write("+$OK#9a")
253				end
254			elseif cmd == "z" then
255				local btype, addr, kind = packet:match("z([0-4]),(%x+),(.*)")
256				addr = tonumber(addr, 16)
257				if btype == "0" then
258					socket:write("") -- is machine dependent
259				elseif btype == "1" then
260					if not breaks.byaddr[addr] then
261						socket:write("+$E00#a5")
262						return
263					end
264					local idx = breaks.byaddr[addr]
265					cpu:debug():bpclr(idx)
266					breaks.byaddr[addr] = nil
267					breaks.byidx[idx] = nil
268					socket:write("+$OK#9a")
269				elseif btype == "2" or btype == "3" or btype == "4" then
270					if not watches.byaddr[addr] then
271						socket:write("+$E00#a5")
272						return
273					end
274					local idx = watches.byaddr[addr]
275					cpu:debug():wpclr(idx)
276					watches.byaddr[addr] = nil
277					watches.byidx[idx] = nil
278					socket:write("+$OK#9a")
279				end
280			elseif cmd == "?" then
281				socket:write("+$S05#B8")
282			else
283				socket:write("+$#00")
284			end
285		end
286	end)
287end
288
289return exports
290