Hi all! I’d like to introduce Snork, a tool I worked on alongside the DumbPad editor last week.
What is Snork?
Snork is a utility for monitoring and logging function calls to system libraries on AOS4. It’s designed for developers and advanced users who need to debug applications or analyze system behavior.
Years ago, on AmigaOS 3, I loved using Gerson Kurz’s Snoopy from 1994 (over 30 years ago!), and I appreciated its script-driven approach, which let me create specific scripts for each debugging task, combining functions from different libraries as needed, etc. However, the original Snoopy was written in 68k assembly, using SetFunction to patch jump tables, and isn’t compatible with AmigaOS 4 of course. So, I created Snork, which adopts a similar script-driven approach but is written in C, using IExec->SetMethod to patch modern library interfaces. It’s not a direct clone of course, but it borrows the idea of script-based control while being built from scratch for AmigaOS 4.
While AmigaOS 4 has tools like glSnoop by Capehill (focused on ogles2.library and warp3dnova.library) and Snoopy by Colin Wenzel (focused on dos.library with a GUI), these are limited to specific libraries. Snork, on the other hand, monitors function calls to system libraries like dos.library, intuition.library, exec.library, and others, with plans to support most system libraries in future versions. In its first version, Snork supports only dos.library and intuition.library and does not handle functions with variable arguments (e.g., tags or ...), support of which and additional libraries is planned for the next version.
Snork reads a configuration script called main.snork to determine which libraries and functions to monitor. Here’s how it operates:
Reads the Script: Parses main.snork to identify libraries (via base= lines) and functions to intercept (via watch= lines),along with logging templates. Patches Functions: Replaces function pointers in library interfaces with wrappers that log parameters before calling the original functions. Logs Parameters: Outputs logs to the serial port using IExec->DebugPrintF. You’ll need a terminal or a tool like Sashimi to view them. Handles Events: Runs in the background, responding to Ctrl+C to stop and clean up resources.
(click open in new tab for fullsize):
This screenshot shows a main.snork script example, the log output, demonstrating how it monitors function calls and part of Snork's guide.
Configuring Snork
Snork is controlled through the main.snork script, where you define libraries and functions to monitor. The scripts directory includes pre-written templates for all supported functions in dos.library and intuition.library. You can copy specific lines into main.snork and edit them as needed.
NOTE: Using all templates at once may slow things down due to heavy serial output, so select only what you need. For fun, you could include everything and watch the serial port get flooded with output!
The script uses two commands at moment:
1. base
Specifies a library and its alias.
Syntax: base=<alias>,<library_name>
alias mean short name (e.g., dos) and library_name mean full name (e.g., dos.library).
Example: base=dos,dos.library
2. watch
Defines a function to monitor and its logging template.
Syntax: watch=<base>,<function>,<template>
base mean library alias, function mean function name (e.g., Write) and template mean Parameter list, e.g., fh=%lp,buf=%lp,len=%ld.
After creating a main.snork script (or using the prebuilt default one for initial tests), run Snork in the shell. You’ll see output like this:
As long as you don’t press Ctrl+C, your serial port will show the debug output you specified.
The archive includes a guide with detailed instructions, so check it out for more information.
Feedback and Future Plans
This is the first beta, and there’s room for improvement. Please share your ideas, findings, and suggestions. What needs to change? What features should be added? Which libraries should come next? (I’m thinking about exec.library, utility.library, and graphics.library.)
Snork has been tested on real hardware (X5000, Pegasos2) and emulated QEMU/Pegasos2, but bugs are possible, of course. If you find issues or have ideas about , report it all plz.
Sounds like a cool tool (love the name!). I like that you can decide which parameters of each function to snoop on, rather than having to see them all, whether you're interested in them or not.
Quote:
Please share your ideas, findings, and suggestions. ... What features should be added?
It would be nice to be able to see the return value from the patched calls, though that will be a bit more complex as you need to report twice, once before the call and once after.
It would also be nice if it showed the name of the program making the call like OS4 Snoopy does, so you can see which calls are made by the program being debugged (and once you can do that, the option to snoop only on a specified program would help filter out clutter from other programs).
A good deal of caution should be employed when displaying a string parameter. Since one of the uses of the program is debugging, you have to consider that in some cases string pointers may not be valid (illegal values other than NULL or -1), or may not point at a real NUL-terminated string.
Somewhere down the road a standalone GUI tool -- something like MPlayer-GUI -- could be created to build the script files and send them to Snork for processing. But that can wait until all the basics are working.
Quote:
Which libraries should come next? (I'm thinking about exec.library, utility.library, and graphics.library.)
A good deal of caution should be employed when displaying a string parameter. Since one of the uses of the program is debugging, you have to consider that in some cases string pointers may not be valid (illegal values other than NULL or -1),
Using a IExec->TypeOfMem() check could partially help: If it returns 0 it's not a virtual address mapped by the kernel, and accessing it will very likely cause a DSI exception.
Quote:
or may not point at a real NUL-terminated string.
Checking that isn't that easy, but possible as well. Requires using such checks for each MMU page, if it's mapped or not. On most, or maybe all, OS4 systems the MMU page size is 4KB.
Just when you CTRL+C Snork just show a simple "Snork ended" on Shell/CLI (and maybe to serial outoput too).
Added, now it says start/end on both serial and console.
@msteed Quote:
It would also be nice if it showed the name of the program making the call like OS4 Snoopy does, so you can see which calls are made by the program being debugged
Done, now output looks like this (some simple trace of few dos functions):
IDOS->Printf("Usage: %s [--help] [--script=<filename>] [--taskinfo=<yes|no>] [--show=<process name>]\n", argv[0]);
IDOS->Printf(" --help Show this help message and exit\n");
IDOS->Printf(" --script=<filename> Script file to parse (must end with .snork, default: main.snork)\n");
IDOS->Printf(" --taskinfo=<yes|no> Show task/process info (default: yes)\n");
IDOS->Printf(" --show=<process name> Log only for specified process name (default: all processes)\n");
IDOS->Printf("Note: For process names with spaces, quote the entire argument, e.g., --show=\"ELF Collector\"\n");
So you can 1), enable/disable task names, 2) use any script you wish (default main.snork), 3) have output from any task you want.
For string handling at minimum will do as Joerg says with IExec->TypeOfMem() , better than nothing (for now).
ps. Btw, added also new keyword for template %t - so you can output tags instead of pointer, and then output will looks like this:
For that had to create a base of the tags for every library (as they can cross, in hope they will not).
And i also move all stuff to IPC now, i.e. in generic_patch just now send the message and call to original, all loging/parsing happens in main's handleinput.
For string handling at minimum will do as Joerg says with IExec->TypeOfMem() , better than nothing (for now).
For dealing with buggy pointers to something that's not a NUL-delimited string, I was thinking of something simple, like limiting the length that's printed to something reasonable, so you won't potentially get thousands of random characters until it happens to encounter a NUL.
In addition, you could filter the string to turn unprintable characters into periods (or some other character), so you won't possibly print an escape code or control character that interferes with the display of the snooped data. That would require making a copy of the string, since you don't want to alter the original.
Quote:
Btw, added also new keyword for template %t - so you can output tags instead of pointer,
Nice; I hadn't even thought of that one. Looking forward to the next update!
For dealing with buggy pointers to something that's not a NUL-delimited string, I was thinking of something simple, like limiting the length that's printed to something reasonable, so you won't potentially get thousands of random characters until it happens to encounter a NUL.
In addition, you could filter the string to turn unprintable characters into periods (or some other character), so you won't possibly print an escape code or control character that interferes with the display of the snooped data. That would require making a copy of the string, since you don't want to alter the original.
For now i limited string to 256 chars, + checking on null and 0xffffffff, + checked on format characters so to printf them as it, without taking as formatting symbols, cheking on fancy characters and replace them to (so printable ASCII character only) , etc. So before show the string i just do this:
Should be enough imho even without IExec->TypeOfMem() ?
I think the idea behind using TypeOfMem() is to catch cases where the string pointer points to something that is not a valid RAM address, where trying to read the characters of the string will probably cause a DSI.
Of course, if the string pointer is invalid the function you're calling will probably cause a DSI itself as soon as you call it. Still, there's some value to Snork not causing the DSI, even if there's going to be one an instant later anyway.
If TypeOfMem() returns zero you can print the string pointer as a hex value, which lets the user see what value was passed and perhaps provides a clue as to what went wrong (assuming the ensuing crash doesn't take down the system).
Of course, if the string pointer is invalid the function you're calling will probably cause a DSI itself as soon as you call it. Still, there's some value to Snork not causing the DSI, even if there's going to be one an instant later anyway.
Or (another suggestion) Snork doesn't pass the invalid pointer but passes on a valid string like "DEADSTRING\0". Than at least no DSI , still the string could mess up other stuff. But still some additional steps before a potential crash.
to add support for international chars, for example the default ISO 8859-15 ones. Unprintable control chars on AmigaOS, in any supported 8 bit charset, are only 0-31 and 127-159.
Of course, if the string pointer is invalid the function you're calling will probably cause a DSI itself as soon as you call it. Still, there's some value to Snork not causing the DSI, even if there's going to be one an instant later anyway.
Some functions may work with invalid string pointers, depending on other arguments the string pointer may not be used at all by the function itself and if it isn't accessed there wont be a DSI. But Snork would cause a DSI trying to print it.
to add support for international chars, for example the default ISO 8859-15 ones. Unprintable control chars on AmigaOS, in any supported 8 bit charset, are only 0-31 and 127-159.
Good catch , thanks !
Btw, what is the best way to follow if we need to parse data in a patch ? At fist, i just doing simple parsing inside of the patch, and then print it before calling original function. Then after , i add some simple IPC via PutMsg() (from patch) /GetMsg() (in main).
At first i tried to simple send what i have to the main, but find out that strings and other data simple died offten (because patch is done, memory changes, but i tried to handle it in the main which is point on whatever else). Because of that i had to copy in the patch before putmsg whole set : libname, functionname, doing findtaks(null), copy strings and tags, and then send this instead. For copy i use IExec->CopyMem/IUtility->Strlcpy (that for libname, funcname and strings), and for tags i simple do simple loop copy.
The question i had now , is it correct way of doing things. I mean, for speed and for being correct at all. As i doing dynamic copy for each patching function, i do not need locks and mutexes, but not sure shouldn't i somehow protect it or not ?