As an example, in this section we will be hacking a very simple utility called elfextractor, that extracts the contents of the sections of an ELF file, whose name is provided as an argument in the command line, into several output files. This is the synopsis of the program:
elfextractor file [section_name]
Where file is the name of the ELF file from which to extract sections, and an optional section_name specifies the name of the section to extract.
Say we have a file foo.o and we would like to extract its text section. We would use elfextractor like:
$ elfextractor foo.o .text
Provided foo.o indeed has a section named .text
, the
utility will create a file foo.o.text with the section’s
contents. Note how the names of the output files are derived
concatenating the name of the input ELF file and the name of the
extracted section.
If no section name is specified, then all sections are extracted. For example:
$ elfextractor foo.o $ ls foo.o* foo.o foo.o.eh_frame foo.o.shstrtab foo.o.symtab foo.o.comment foo.o.rela.eh_frame foo.o.strtab foo.o.text
This is a possible implementation of elfextractor:
#!/usr/bin/poke -L !# /* elfextractor - Extract sections from ELF64 files. */ load elf; if (!(argv'length in [1UL,2UL])) { print "Usage: elfextractor FILE [SECTION_NAME]\n"; exit (1); } var file_name = argv[0]; var section_name = (argv'length > 1) ? argv[1] : ""; try { var fd = open (file_name, IOS_M_RDONLY); var elf = Elf64_File @ fd : 0#B; for (shdr in elf.shdr where shdr.sh_type != 0x0) { var sname = elf.get_section_name (shdr.sh_name); if (section_name == "" || sname == section_name) save :ios elf'ios :file file_name + sname :from shdr.sh_offset :size shdr.sh_size; } close (fd); } catch (Exception e) { if (e == E_constraint) printf ("error: `%s' is not a valid ELF64 file\n", file_name); else if (e == E_io) printf ("error: couldn't open file `%s'\n", file_name); else raise e; exit (1); }
First the command line arguments are handled. The script checks
whether the right number of arguments have been passed (either 1 or 2)
exiting with an error code otherwise. The file name and the section
name are then extracted from the argv
array.
Once we have the file name and the optional desired section name, it is time to do the real work. The code is enclosed in a try-catch block statement, because some of the operations may result on exceptions being raised.
First, the ELF file whose name is specified in the command line is opened for reading:
var fd = open (file_name, IOS_M_RDONLY);
The built-in function open
returns a file descriptor that can
be subsequently used in mapping operations. If the provided file name
doesn’t identify a file, or if the file can’t be read for whatever
reason, an E_io
exception is raised. Note how the exception is
handled in the catch
block, emitting an appropriate diagnostic
message and exiting with an error status.
Once the ELF file is open for reading, we map an Elf64_File
on
it, at the expected offset (zero bytes from the beginning of the
file):
var elf = Elf64_File @ fd : 0#B;
If the file doesn’t contain valid ELF data, this map will fail and
raise an E_constraint
exception. Again, the catch
block
handles this situation.
At this point the variable elf
contains an Elf64_File
.
Since we want to extract the sections contained in the file, we need
to somehow iterate on them. The section header table is available in
elf.shdr
. A for-in-where loop is used to iterate on all the
headers, skipping the “null” ELF sections which are always empty,
and are characterized by a shdr.sh_type
of 0. An inner
conditional filters out sections whose name do not match the provided
name in the command line, if it was specified at all.
For each matching section we then save its contents in a file named
after the input ELF file, by calling a function save
, which is
provided by poke:
save :ios elf'ios :file file_name + sname :from shdr.sh_offset :size shdr.sh_size;
The above is exactly what we would have written at the poke REPL!
(modulus trailing semicolon). How is this supposed to work? Thing
is, GNU poke commands are implemented as Poke functions. Let’s
consider save
, for example. It is defined as a function having
the following prototype:
fun save = (int ios = get_ios, string file = "", off64 from = 0#B, off64 size = 0#B, int append = 0, int verbose = 0) void: { ... }
Once a Poke function is defined in the environment, it becomes available as such. Therefore, in a poke session we could call it like:
(poke) save (get_ios, "filename", 0#B, 12#B, 0, 1)
However, this is cumbersome and error prone. To begin with, we should
remember the name, position and nature of each argument accepted by
the command. What is even more annoying, we are forced to provide
explicit values for them, like in the example above we have to pass
the current IOS (the default), and 0 for append
(the default)
just to being able to set verbose
. Too bad.
To ease commanding poke, the Poke language supports an alternative syntax to call functions, in which the function arguments are referred by name, can be given in any order, and can be omitted. The command above can be thus written like:
(poke) save :from 0#B :size 12#B :verbose 1
This syntax is mostly intended to be used interactively, but nothing prevents to use it in Poke programs and scripts whenever it is deemed appropriate, like we did in elfextractor. We could of course have used the more conventional syntax:
if (section_name == "" || sname == section_name) save (elf'ios, file_name + sname, shdr.sh_offset, shdr.sh_size, 0, 0);
What style to use is certainly a matter of taste.
Anyhow, once the sections have been written out, the file descriptor
is closed and the program exits with the default status, which is
success. Should the save
function find any problem saving the
data, such as a full disk, not enough permissions or the like,
exceptions will be raised, caught and maybe handled by our
catch
block.
And this is it! The complete program is 44 lines long. This is a good example that shows how, given a pickle providing a reasonable description of some binary-oriented format (ELF in this case) poke can be leveraged to achieve a lot in a very concise way, free from the many details involved in the encoding, reading and writing of binary data.