3

Suppose I have some text like this (output of objdump -d):

   0:   0f a2                   cpuid  
   2:   a9 01 00 00 00          test   eax,0x1
   7:   74 01                   je     a <myFunc+0xa>
   9:   c3                      ret    
   a:   0f 0b                   ud2a

I'd like to replace the text like ^ +[0-9a-f]+: with corresponding number of spaces (so as to preserve the length), but only if the part before : isn't mentioned anywhere else (as a word, i.e. enclosed in word boundaries). E.g. in the above example the labels 0,2,7,9 would be replaced with a space and a would remain intact (since it's mentioned in the third line).

Here's how the above example would look after processing:

        0f a2                   cpuid  
        a9 01 00 00 00          test   eax,0x1
        74 01                   je     a <myFunc+0xa>
        c3                      ret    
   a:   0f 0b                   ud2a

Is there any nicer way to do it in shell/vim than counting occurrences of the labels and then processing the lines based on these counts?

My current code processes 2300-line file in 3 minutes (on Intel Atom CPU), which is much too long:

#!/bin/bash -e

if [ $# -ne 2 ]; then
    echo "Usage: $0 infile outfile" >&2
    exit 1
fi

file="$1"
outfile="$2"

cp "$file" "$outfile"

labelLength=$(sed -n '/^ \+\([0-9a-f]\+\):.*/{s@@\1@p;q}' "$file"|wc -c)
replacement=$(printf %${labelLength}c ' ')

sed 's@^ \+\([0-9a-f]\+\):.*@\1@' "$file" | while read label
do
    if [ $(grep -c "\<$label\>" "$file") = 1 ]; then
        sed -i "s@\<$label\>:@$replacement@" "$outfile"
    fi
done
  • What's the corresponding number of spaces? Corresponding to what? The number of characters in the label? And why would 0,2,7 and 9 be replaced? They all occur in the file. What about multi-character labels? Should the be left alone if any of their characters occurs in the file or only if the entire string does? – terdon Apr 6 '16 at 7:48
  • @terdon I've edited to clarify. The 0,2,7,9 would be replaced because they are mentioned as single words (\b[0-9a-f]+\b) only once. Multi-character labels should be considered as whole words. – Ruslan Apr 6 '16 at 7:54
  • Ah, I see. Please edit your question and i) show your desired output, that makes it much easier to understand and ii) include an example showing what should happen with multi-character labels. – terdon Apr 6 '16 at 7:58
  • @terdon added an example of output and my own (suboptimal) code to do the task. – Ruslan Apr 6 '16 at 9:04
2

A Perl solution:

$ perl -lne '$k{"$_:"}++ for split(/\b/); push @l,$_; }{
   map{s/\S+:/$k{$&}<2 ? " " x length($&) : $&/e; print}@l;' file
     0f a2                   cpuid  
     a9 01 00 00 00          test   eax,0x1
     74 01                   je     a <myFunc+0xa>
     c3                      ret    
a:   0f 0b                   ud2a

This is equivalent to:

#!/usr/bin/perl
use strict;
my %wordsHash;
my @lines;
## Read the input file line by line, saving each
## line as $_. This is what 'perl -n` means.
while (<>) {
    ## Remove trailing newlines. This is done
    ## by -l in the one liner.
    chomp;
    ## Split the current line on word boundaries
    my @wordsArray=split(/\b/);
    ## Save each word + ":" as a key in the hash %wordsHash,
    ## incrementing the value by one each time the word
    ## is seen.
    foreach my $word (@wordsArray) {
        $wordsHash{"$word:"}++;
    }
    ## Add the line to the array @lines
    push @lines, $_;
}

## After the file has been read. ('}{' in the one-liner)
## Iterate over each line in @lines ( map{}@l in the one-liner)
foreach my $line (@lines) {
    ## Grab the first set of non-whitespace
    ## characters until the 1st ':'
    $line=~/\S+:/;
    ## $& is whatever was matched
    my $match=$&;
    ## If the match was seen only once
    ## as a word (all will be seen at least once)
    if ($wordsHash{$match}<2) {
        ## The replacement is as many spaces
        ## as $match has characters.
        my $rep = " " x length($match);
        ## Replace it in the line
        $line=~s/$match/$rep/;
    }
    ## Print the line
    print "$line\n";;
}
  • For some reason, the one-liner appears to consider file:line things as label:code, unlike the expanded version, which preserves file: parts in some cases. – Ruslan Apr 13 '16 at 14:33
  • @Ruslan what's file:line? I'd be happy to check it out but you would need to give an example. – terdon Apr 13 '16 at 14:40
  • It's what objdump gives with the -l option. Actually I've fixed locally in the expanded version by only adding $word to the map if($word =~ /[0-9a-f]+/), and changing the search $line to $line=~/[0-9a-f]+:/; instead of the original $line=~/\S+:/;. – Ruslan Apr 13 '16 at 14:42
  • Here's an example: paste.ubuntu.com/15813993 – Ruslan Apr 13 '16 at 15:52
2

Reading between the lines, I assume you want to make your disassembly more readable by removing addresses on lines that are not referred to by jump and similar instructions. This awk assumes the number in the last but one column is an address when the last column begins "<". It reads the disassembly once, remembering all such addresses in an array, then a second time replacing the address at the start of the line if it didn't appear.

$ objdump -d /bin/ls >/tmp/a
$ awk '
NR==FNR { if($NF ~ /</) address[$(NF-1)] = 1; next }
$1 ~ /:/ { if(!address[substr($1,1,length($1)-1)]){
              i = index($0,":")
              printf "%*s%s\n",i," ",substr($0,i+1)
              next }
        }
{ print }
' /tmp/a /tmp/a

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.