{"id":544,"date":"2024-05-26T23:18:44","date_gmt":"2024-05-27T04:18:44","guid":{"rendered":"https:\/\/greg-kennedy.com\/wordpress\/?p=544"},"modified":"2024-05-27T00:25:52","modified_gmt":"2024-05-27T05:25:52","slug":"static-recompilation-of-chip-8-programs","status":"publish","type":"post","link":"https:\/\/greg-kennedy.com\/wordpress\/2024\/05\/26\/static-recompilation-of-chip-8-programs\/","title":{"rendered":"Static Recompilation of CHIP-8 Programs"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"551\" src=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo-1024x551.png\" alt=\"\" class=\"wp-image-545\" srcset=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo-1024x551.png 1024w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo-300x161.png 300w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo-768x413.png 768w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo-1536x826.png 1536w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo-624x336.png 624w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.png 1920w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Screenshot of &#8220;UFO.ch8&#8221; running natively in a terminal<\/figcaption><\/figure>\n\n\n\n<p>&#8220;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Binary_translation#Static_binary_translation\">Static Recompliation<\/a>&#8221; is the process of turning a binary program built for one instruction set and CPU into equivalent native code for a different architecture.  It&#8217;s a hot topic lately as news of a static recompilation process for N64 games &#8211; turning the original MIPS code into x86 executables for Windows &#8211; seemingly promises an entirely new level of features and performance beyond existing emulation techniques.  I&#8217;ve been interested in the topic since hearing of the <a href=\"https:\/\/hackaday.com\/2014\/07\/31\/playing-starcraft-on-an-arm\/\">StarCraft on ARM recompilation<\/a> for OpenPandora in 2014, but not looked too much further into the idea until recently.  I decided to try my own hand at a static recompiler, where I would be transforming some machine ROM into C source code, then compile that to produce native executables.<\/p>\n\n\n\n<p>First, I&#8217;d need a source system to use: The <a href=\"https:\/\/en.wikipedia.org\/wiki\/CHIP-8\">CHIP-8<\/a> VM, often considered a good &#8220;baby&#8217;s first emulator&#8221; project.  It is an interpreted 8-bit machine, with only 30 opcodes or so, limited graphics and sound capabilities and a lot of tooling available.  I began by spending a couple of days implementing the machine in C as a plain &#8220;interpreted&#8221; emulator, using libcurses to handle keyboard input and display visuals on the terminal.  This was useful for getting a feel for the machine, learning about quirks and addressing modes, as well as a benchmarking test.  Some of the routines for interfacing the VM with the real hardware (e.g. &#8220;print screen&#8221;, &#8220;check keypress&#8221; etc) are theoretically reusable in a recompiled version as well.  CHIP-8 emulation is a well-studied topic, I won&#8217;t say much more about it here.<\/p>\n\n\n\n<p>With that complete, I turned my attention to the recompilation.  If you&#8217;d like to see the code I wrote, I&#8217;ve placed it in GitHub here:<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/greg-kennedy\/CURSE-8\">https:\/\/github.com\/greg-kennedy\/CURSE-8<\/a><\/p>\n\n\n\n<p>Inside are the interpreter, and a Perl script &#8220;recompile.pl&#8221; which turns a .ch8 file into a .c.  Compiling this along with &#8220;wrapper.c&#8221; and linking to libcurses should give a runnable executable of the chip8 program.<\/p>\n\n\n\n<p>Details of approaches to static recompilation follow.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h1 class=\"wp-block-heading\">Basic Plan<\/h1>\n\n\n\n<p>A first approach to do this is something like so &#8211; a portion of the common &#8220;IBM Logo&#8221; program.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#include &lt;stdint.h&gt;\n\nuint8_t RAM&#91;4096] = { 0xF0, 0x90, 0x90, 0x90, 0xF0, \/\/ the starting font\n                      0x20, 0x60, 0x20, 0x20, ...,\n                      0x00, 0x00, 0x00, 0x00, 0x00, \/\/ padding to 0x200\n                      0x00, 0x00, 0x00, 0x00, ...,\n                      0x00, 0xe0, 0xa2, 0x2a, 0x60, \/\/ the contents of the ROM\n                      0x0c, 0x61, 0x08, 0xd0, ...\n                    };\n\nvoid main() {\n  \/\/ machine variables\n  uint16_t I;\n  uint8_t v&#91;0x10];\n  uint8_t timer_delay = 0, timer_sound = 0;\n\nlbl_200:            \/\/ decoded instruction at 0x200\n    screen_clear();\nlbl_202:\n    I = RAM&#91;0x22a];\nlbl_204:\n    v&#91;0x0] = 0x0c;\n\/\/ remainder of disassembly\n...\nlbl_280:\n    screen_clear();\nlbl_282:\n    screen_clear();\n\n\/\/ alternate alignment decoding\nlbl_201:\n    return;  \/\/ ILLEGAL OPCODE\nlbl_203:\n    \/\/ CALL lbl_a60;\n...\n}<\/code><\/pre>\n\n\n\n<p>This copies the ROM into a block at the start of the program, then literally translates each opcode from 0x200 onwards into C code.  It&#8217;s done twice, because CHIP-8 has no alignment restrictions but all opcodes are 2 bytes &#8211; and so the instructions after a JMP could be at odd-numbered locations instead.<\/p>\n\n\n\n<p>Much of this is pretty straightforward, but there are two issues relating to control flow that deserve special mention:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>CALL and RETURN opcodes.<br>CALL and RETURN alter the PC on CHIP-8, but C code doesn&#8217;t allow that flexibility with &#8220;goto&#8221;.  You can accomplish this with <a href=\"https:\/\/en.wikipedia.org\/wiki\/Setjmp.h\">setjmp \/ longjmp<\/a> instead.  An array of jmp_buf and SP stack pointer works, like this, to &#8220;set&#8221; the return state on the stack and then later &#8220;longjmp&#8221; back to it.<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>#define STACK_DEPTH 16\njmp_buf stack&#91;STACK_DEPTH];\nuint8_t sp = 0;\n\n#define CALL(x) if (setjmp(stack&#91;sp])) sp --; else { sp ++; goto lbl_ # x; }\n#define RET() longjmp(stack&#91;sp - 1], sp);<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Computed JMP opcode (BNNN)<br>CHIP-8 supports one opcode that allows computed JMP, by adding register v[0] to literal address NNN. Instruction sizes on the target machine may differ from those on source machine, so it&#8217;s not possible to just &#8220;add to the host Program Counter&#8221; as that won&#8217;t end up in the right spot. One way of handling this is to build a &#8220;jump table&#8221;: a big switch table and goto for each v0 value:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>lbl_400:    \/\/ consider opcode = \"B500\"\n  switch(v&#91;0]) {\n    case 0: goto lbl_500;\n    case 1: goto lbl_501;\n    case 2: goto lbl_502;\n    ...\n    case 255: goto lbl_5FF;\n  }<\/code><\/pre>\n\n\n\n<h1 class=\"wp-block-heading\">Improvements<\/h1>\n\n\n\n<p>The above &#8220;works&#8221; but it&#8217;s very naive: it includes an entire copy of the ROM in the output binary, and it disassembles opcodes that don&#8217;t need disassembly (twice!).  Wasteful, and hard to follow!  Going back through this to clean up the source will be a nightmare.<\/p>\n\n\n\n<p>Instead let&#8217;s adopt a two-step process, beginning with &#8220;static analysis&#8221; (parsing the binary to identify code paths and data regions) and then the translation to C source afterwards.  Because CHIP-8 mixes instructions and data freely (curse you, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Von_Neumann_architecture\">von Neumann<\/a>) it&#8217;s necessary to traverse the binary from the starting position 0x200, mark instructions as they&#8217;re crossed by the PC, and data as read \/ write when accessed by an instruction.<\/p>\n\n\n\n<p>To figure out which instructions need translating, trace execution through the program and mark areas potentially visited by the PC.  An example of doing the analysis, in Perl:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env perl\nuse strict;\nuse warnings;\n\nmy @ram = load_rom();\n\nmy @map = ( (0) x 4096 );\n\nsub trace {\n  my $pc = shift;\n  while (1) {\n    # instruction already visited, terminate\n    return if ($map&#91;$pc] == 1);\n\n    # mark this location as Visited\n    $map&#91;$pc] = 1;\n\n    # switch based on instruction types that affect the PC\n    if ($ram&#91;$pc] == JUMP) {\n      # directly set PC to the address in the jump\n      $pc = $address;\n    } elsif ($ram&#91;$pc] == CALL) {\n      # trace into the subroutine, then return to here and continue\n      trace($addresss);\n      $pc += 2;\n    } elsif ($ram&#91;$pc] == RETURN) {\n      # return from subroutine call\n      return;\n    } elsif ($ram&#91;$pc] == CONDITIONAL) {\n      # one of the instructions that conditionally skips the next line\n      #  since we cannot know in advance whether the jump is taken,\n      #  evaluate both branches\n      trace($addresss);\n      $pc += 2;      \n    } elsif ($ram&#91;$pc] == COMPUTED_JUMP) {\n      # as this could land anywhere from ADDR to ADDR + 255, step into each\n      #  assume alignment (v&#91;0] is even)\n      for (my $v = 2; $v &lt; 255; $v += 2) {\n        trace($address + $v);\n      }\n      # continue at address\n      $pc = $address;\n    } else {\n      # other opcodes do not change control flow\n      $pc += 2;\n    }\n  }\n}\n\ntrace(0x200);\n\n# @map now contains a 1 for every position containing an instruction\n#  translate only those marked into code\n# a further enhancement is to mark RAM access (read, write) as well\n#  translate those blocks to C arrays and ignore any other<\/code><\/pre>\n\n\n\n<p>With a map in hand it&#8217;s possible to more intelligently turn the ROM data into C code.  There is no need to translate unvisited values into instructions, nor to include the entire RAM contents &#8211; only those touched by read\/write operations.  <\/p>\n\n\n\n<p>Here&#8217;s a visual I wrote that shows the RAM layout for analyzed ROMs.  First, IBM Logo:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"435\" src=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ibmlogo-1024x435.png\" alt=\"\" class=\"wp-image-546\" srcset=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ibmlogo-1024x435.png 1024w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ibmlogo-300x127.png 300w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ibmlogo-768x326.png 768w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ibmlogo-1536x652.png 1536w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ibmlogo-624x265.png 624w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ibmlogo.png 1743w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Table of IBM Logo analysis, showing first bytes of Code data (blue) and remaining bytes Read data (red)<\/figcaption><\/figure>\n\n\n\n<p>and then, UFO.ch8, a more advanced program with user input and multiple control changes:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"451\" src=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_-1024x451.png\" alt=\"\" class=\"wp-image-547\" srcset=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_-1024x451.png 1024w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_-300x132.png 300w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_-768x338.png 768w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_-1536x676.png 1536w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_-624x275.png 624w, https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/ufo.ch8_.png 1747w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">UFO.ch8, mostly code, red blocks for the sprite data, and a yellow area (read \/ write) used as scratch space for score display<\/figcaption><\/figure>\n\n\n\n<p>Static analysis brings other advantages as well, like finding dead code, uninitialized RAM reads, and the PC running off the memory page.  Note the two bytes surrounding the sprite data (above), which are unused bytes.<\/p>\n\n\n\n<p>For reference, here&#8217;s the C recompilation of IBM Logo after using the tracing as above.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void run() {\n  uint8_t ram_22a&#91;] = { 0xff, 0x00, 0xff, 0x00, 0x3c, 0x00, 0x3c, 0x00, 0x3c, 0x00, 0x3c, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0x38, 0x00, 0x3f, 0x00, 0x3f, 0x00, 0x38, 0x00, 0xff, 0x00, 0xff, 0x80, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0x80, 0x00, 0x80, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0x80, 0xf8, 0x00, 0xfc, 0x00, 0x3e, 0x00, 0x3f, 0x00, 0x3b, 0x00, 0x39, 0x00, 0xf8, 0x00, 0xf8, 0x03, 0x00, 0x07, 0x00, 0x0f, 0x00, 0xbf, 0x00, 0xfb, 0x00, 0xf3, 0x00, 0xe3, 0x00, 0x43, 0xe0, 0x00, 0xe0, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xe0, 0x00, 0xe0 };\n\nlbl_200:\n        screen_clear();\nlbl_202:\n        i = ram_22a + 0x000;\nlbl_204:\n        v&#91;0x00] = 0x0c;\nlbl_206:\n        v&#91;0x01] = 0x08;\nlbl_208:\n        screen_sprite( v&#91;0x0], v&#91;0x1], 0x0f );\nlbl_20a:\n        v&#91;0x00] += 0x09;\nlbl_20c:\n        i = ram_22a + 0x00f;\nlbl_20e:\n        screen_sprite( v&#91;0x0], v&#91;0x1], 0x0f );\nlbl_210:\n        i = ram_22a + 0x01e;\nlbl_212:\n        v&#91;0x00] += 0x08;\nlbl_214:\n        screen_sprite( v&#91;0x0], v&#91;0x1], 0x0f );\nlbl_216:\n        v&#91;0x00] += 0x04;\nlbl_218:\n        i = ram_22a + 0x02d;\nlbl_21a:\n        screen_sprite( v&#91;0x0], v&#91;0x1], 0x0f );\nlbl_21c:\n        v&#91;0x00] += 0x08;\nlbl_21e:\n        i = ram_22a + 0x03c;\nlbl_220:\n        screen_sprite( v&#91;0x0], v&#91;0x1], 0x0f );\nlbl_222:\n        v&#91;0x00] += 0x08;\nlbl_224:\n        i = ram_22a + 0x04b;\nlbl_226:\n        screen_sprite( v&#91;0x0], v&#91;0x1], 0x0f );\nlbl_228:\n        return;\n}\n<\/code><\/pre>\n\n\n\n<p>Much simpler, and more accurate, than the first attempt!<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Further Analysis with Bit Sets<\/h1>\n\n\n\n<p>It helps to think of the main goal of the analyzer as &#8220;reducing the amount of decompilation&#8221;.  In the first pass we decompiled <em>everything<\/em>, and included the entire RAM contents, even though most of it was unnecessary or not actually executed.  The instruction tracing is still missing things and could be refined further.  Consider this assembly fragment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>v0 := 0x05\nif v0 == 0x06 then\n    jump is_six\njump is_five\n...\n: is_five\n  # some instructions\n: is_six\n  # different instructions<\/code><\/pre>\n\n\n\n<p>With the code before, both paths will be evaluated.  As a human looking at this, we recognize that the condition &#8220;if v0 == 0x06&#8221; will never be true, so the branch is never taken, and is_six is entirely dead code.<\/p>\n\n\n\n<p>I improved on this by having the analyzer carry additional state data along during tracing: for each register, a 256-bit block where each bit position indicates a &#8220;possible&#8221; value held in the register at that point.  So, for example, &#8220;v0 := 0x5&#8221; will clear the bit vector for v0 and then set position 5 to a 1 value, as the instruction eliminates all possibilities except 5 at that point.  The subsequent check, &#8220;v0 == 0x06&#8221;, is done by ANDing the v0 with a bit vector in which only position 6 is set.  The result vector &#8211; always 0 &#8211; indicates that the condition is never true, the jump never taken, and the entire branch eliminated.<\/p>\n\n\n\n<p>The same change was made for I register, which has 4096 bits instead.  Also, instead of returning when the PC has visited a location, it instead must check if the machine has already (potentially) been in the current exact state already, including the stack, through use of bit operations vs the RAM map.  This gets very memory demanding!  Decompiling UFO takes about 18 seconds on my machine, and I haven&#8217;t been able to get it to disassemble Chipquarium.ch8 successfully yet&#8230; oh well.  Maybe if I rewrite this in C instead of Perl \ud83d\ude42<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Self-Modifying Code<\/h1>\n\n\n\n<p>There&#8217;s an elephant in the room in all this.  Consider this assembly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>: main\n i := 0x207\n v0 := 5\n save v0\n v1 := 0\n i := hex v1\n sprite v0 v0 5<\/code><\/pre>\n\n\n\n<p>This is an example of self-modifying code.  The &#8220;save v0&#8221; instruction places the value 0x05 at location 0x207 &#8211; replacing the 0 in &#8220;v1 := 0&#8221; with &#8220;v1 := 5&#8221;.  Running this program on an interpreter will show the number 5 on the screen.  Running it with the decompiler shows 0, because the RAM address 0x207 no longer has anything to do with the code we generated and compiled.<\/p>\n\n\n\n<p>There are solutions to this, like embedding an interpreter in your machine that works for self-modifying regions and falls back to the precompiled stuff otherwise.  But I made a simpler decision: attempting to &#8220;write&#8221; an exec region, or &#8220;exec&#8221; a written region, just kills the analyzer instead.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code> $ .\/recompile.pl slipperyslope.ch8\nDeep recursion on subroutine \"main::iterate\" at .\/recompile.pl line 282.\nWrite to executable memory 613 + 0 at 690, op = f055 at .\/recompile.pl line 668.<\/code><\/pre>\n\n\n\n<p>This unfortunately locks me out of a lot of newer CHIP-8 games, which use self-modifying code to change sprite tables and data regions.  But at least I can play UFO.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Benchmarks<\/h1>\n\n\n\n<p>What&#8217;s the point of all this if it doesn&#8217;t go faster than pure interpretation? For a rough benchmark I used the &#8220;1dcell.ch8&#8221; program, which plots the results of each Rule of <a href=\"https:\/\/en.wikipedia.org\/wiki\/Elementary_cellular_automaton\">elementary cellular automata<\/a> in a loop. Well, it&#8217;s difficult to do actual measurements (not least because ssh and curses make it very I\/O bound), but here&#8217;s the program recompiled and running at top speed:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/1dcell.opti_.gif\"><img loading=\"lazy\" decoding=\"async\" width=\"639\" height=\"636\" src=\"https:\/\/greg-kennedy.com\/wordpress\/wp-content\/uploads\/2024\/05\/1dcell.opti_.gif\" alt=\"\" class=\"wp-image-548\"\/><\/a><figcaption class=\"wp-element-caption\">1dcell.ch8 recompiled and running at extremely high speed<\/figcaption><\/figure>\n\n\n\n<p>Anyway, I&#8217;m satisfied with this little experiment for now &#8211; while I have some ideas for further refinements, it certainly seems proven possible, and I have other projects to consider \ud83d\ude42<\/p>\n","protected":false},"excerpt":{"rendered":"<p>&#8220;Static Recompliation&#8221; is the process of turning a binary program built for one instruction set and CPU into equivalent native code for a different architecture. It&#8217;s a hot topic lately as news of a static recompilation process for N64 games &#8211; turning the original MIPS code into x86 executables for Windows &#8211; seemingly promises an [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8],"tags":[],"class_list":["post-544","post","type-post","status-publish","format-standard","hentry","category-software"],"_links":{"self":[{"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/posts\/544","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/comments?post=544"}],"version-history":[{"count":2,"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/posts\/544\/revisions"}],"predecessor-version":[{"id":551,"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/posts\/544\/revisions\/551"}],"wp:attachment":[{"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/media?parent=544"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/categories?post=544"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/greg-kennedy.com\/wordpress\/wp-json\/wp\/v2\/tags?post=544"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}