Monday, November 9, 2009

How to Get the Version of a Kernel Without Booting It

Sometimes you need to get version from a kernel file lying on filesystem and the compiler that was used to compile it, but there is no opportunity to boot from this kernel. For example, once I recompiled my own kernel with some options in .config changed, and I wanted to be sure that new kernel's vermagic string doesn't differ from modules' one. This string is usually being printed by Linux kernel in the beginning of boot, e.g.:
Linux version 2.6.24-19-generic (buildd@terranova) (gcc version 4.2.3 (Ubuntu 4.2.3-2ubuntu7)) #1 SMP Fri Jul 11 23:41:49 UTC 2008 (Ubuntu 2.6.24-19.36-generic)


To know more about vermagic please read chapter 2.8 of The Linux Kernel Module Programming Guide and description of --force-vermagic option in modprobe (8).

To get the vermagic string of some module on a disk, use modinfo module.ko (even if this module was built for other arch, e.g. we work in i386 and modules is for x86_64, and vice versa). But you can not use this trick (or invent a similar one) with a kernel file because modinfo doesn't work with vmlinuz.

But it's not so hard as you can imagine :)

vmlinuz consists of some parts: loader, decompressor and the compressed (zlib) kernel itself. Gzipped content usually begins with byte sequence 1f 8b 08 00. We can use od utility to find them:
od -A d -t x1 vmlinuz|grep "1f 8b 08 00"

But often these bytes may occur in the different lines, like in the following fragment (was taken from Ubuntu Hardy's generic kernel):
0011376 00 fd f3 a4 fc 5e 81 c5 ff ff 0f 00 81 e5 00 00
0011392 f0 ff 8d 83 90 01 1d 00 ff e0 01 01 1d 00 1f 8b
0011408 08 00 26 a1 ac 48 02 03 ec 3a 6d 74 14 55 96 af
0011424 3b d5 49 77 e8 58 15 a7 5b 5b b6 19 1a 2d 30 19


So we will use an alternate method.

Kernel source includes utility named scripts/binoffset.c that does exactly what we need: find the first occurrence of given bytes in the bytes stream. This utility's source is included in the Debian package linux-headers, so you don't need to grab and extract full kernel source.

Let's use it:
$ gcc -o binoffset /usr/src/linux/scripts/binoffset.c
$ ./binoffset /vmlinuz 0x1f 0x8b 0x08 00 2>/dev/null
11406

It means that gzip header begins at offset 11406 from the beginning of the file.

Now extract the compressed kernel from vmlinuz and decompress it:
dd if=/vmlinuz skip=11406 bs=1|gzip -d >vmlinux

And find our string there, using strings + grep:
$ strings vmlinux |grep gcc
Linux version 2.6.24-19-generic (buildd@terranova) (gcc version 4.2.3 (Ubuntu 4.2.3-2ubuntu7)) #1 SMP Wed Aug 20 22:56:21 UTC 2008 (Ubuntu 2.6.24-19.41-generic)
%s version %s (buildd@terranova) (gcc version 4.2.3 (Ubuntu 4.2.3-2ubuntu7)) %s

So, we have got what we want :) Let's concatenate all this stuff to one line:
(dd if=/vmlinuz skip=`./binoffset /vmlinuz 0x1f 0x8b 0x08 0x00` bs=1|gzip -d |strings|grep -n 1"^Linux version") 2>/dev/null

P.S. /usr/src/linux/scripts/extract-ikconfig works like described here, but it needs some modifications to make it do the thing we want... and I like to reinvent the bicycles :)

Wednesday, June 10, 2009

Using command_not_found_handle to VERY quick access hosts via ssh.

Some time ago, I had noticed that my .bash_history contains a lot of similar records like 'ssh 192.168.0.10', 'ssh 192.168.0.24' etc. So I thought that this is not the True Unix Way and attempted to change this situation.

First of all, I generated a list of bash aliases with entries like the following:

...
alias 123='ssh user@192.168.0.123'
alias 125='ssh user@192.168.0.125'
...


But this solution looked too ugly. Another solution was to add lots of ssh_config entries as described here; it was ugly too.

Another thing that made these two hacks useless was that I needed to connect to hosts in other networks, like 192.168.1.x, 192.168.20.x etc, and it wasn't possible to add all of them to .bash_aliases or ~/.ssh/config.

So I invented the wheel.

In Debian (and Ubuntu too) there is a valuable addendum to bash: function command_not_found_handle that accepts the name of non-existing command and decides what to do: call another program instead this command or something else. By default, it searches the command name in apt package cache and advices user to install the appropriate package.

I wrote my own function and placed it to ~/.bashrc:
command_not_found_handle () {
if [[ ! "$1" ]] ; then
return 127
fi

n="$1"

if echo $n| perl -ne 'exit(/^([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/ ? 0:1)' ; then
ip=192.168.20.$n
elif echo $n| perl -ne '
exit (/^([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.
([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])
$/x ? 0:1)' ; then
ip=192.168.$n
elif echo $n| perl -ne '
exit (/^([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.
([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.
([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.
([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])
$/x ? 0:1)' ; then
ip=$n
else
return 127
fi

ssh $ip
}


How does it work. The function analyzes its argument, and if this argument is a number from 0 to 255, it calls "ssh 192.168.0.xx" (where xx is the argument passed to this function); if the argument passes contains two dot-separated numbers like xx.xx (each is from 0 to 255), it calls ssh 192.168.xx.xx. If the argument validates as a correct IP-address, "ssh ip" is called. In any other cases, exit code 127 is returned and bash handles "command not found" error by itself.

So, I write only the last number of the IP address and it automatically being translated to the full IP:
bvk@host ~$ 15
# becomes ssh 192.168.0.15
bvk@host ~$ 29
# becomes ssh 192.168.0.29
bvk@host ~$ 10.51
# becomes ssh 192.168.10.51
bvk@host ~$ 172.19.4.10
# becomes ssh 172.19.4.10

Also, I wrote some entries to ~/.ssh/config to specify different parameters for different hosts:

Host 192.168.20.251
User special_user1

Host 192.168.20.252
User special_user2

Host 192.168.20.254
User special_user3

Host *
User ordinary_user


This allows me to connect to the hosts 192.168.20.251, 192.168.20.252 and 192.168.20.253 as respective users, and to all other hosts as ordinary_user.

P.S. It's often impossible to give names to all the hosts in our networks, because hosts in our test environment go up and down frequently. So we have to connect to them via their IPs.