fix-pintos-script-geometry.patch (applied cleanly)
[pintos-anon] / src / utils / Pintos.pm
1 # Pintos helper subroutines.
2
3 # Number of bytes available for the loader at the beginning of the MBR.
4 # Kernel command-line arguments follow the loader.
5 our $LOADER_SIZE = 314;
6
7 # Partition types.
8 my (%role2type) = (KERNEL => 0x20,
9                    FILESYS => 0x21,
10                    SCRATCH => 0x22,
11                    SWAP => 0x23);
12 my (%type2role) = reverse %role2type;
13
14 # Order of roles within a given disk.
15 our (@role_order) = qw (KERNEL FILESYS SCRATCH SWAP);
16
17 # Partitions.
18 #
19 # Valid keys are KERNEL, FILESYS, SCRATCH, SWAP.  Only those
20 # partitions which are in use are included.
21 #
22 # Each value is a reference to a hash.  If the partition's contents
23 # are to be obtained from a file (that will be copied into a new
24 # virtual disk), then the hash contains:
25 #
26 # FILE => name of file from which the partition's contents are copied
27 #         (perhaps "/dev/zero"),
28 # OFFSET => offset in bytes in FILE,
29 # BYTES => size in bytes of contents from FILE,
30 #
31 # If the partition is taken from a virtual disk directly, then it
32 # contains the following.  The same keys are also filled in once a
33 # file-based partition has been copied into a new virtual disk:
34 #
35 # DISK => name of virtual disk file,
36 # START => sector offset of start of partition within DISK,
37 # SECTORS => number of sectors of partition within DISK, which is usually
38 #            greater than round_up (BYTES, 512) due to padding.
39 our (%parts);
40
41 # set_part($opt, $arg)
42 #
43 # For use as a helper function for Getopt::Long::GetOptions to set
44 # disk sources.
45 sub set_part {
46     my ($opt, $arg) = @_;
47     my ($role, $source) = $opt =~ /^([a-z]+)(?:-([a-z]+))?/ or die;
48
49     $role = uc $role;
50     $source = 'FILE' if $source eq '';
51
52     die "can't have two sources for \L$role\E partition"
53       if exists $parts{$role};
54
55     do_set_part ($role, $source, $arg);
56 }
57
58 # do_set_part($role, $source, $arg)
59 #
60 # Sets partition $role as coming from $source (one of 'file', 'from',
61 # or 'size').  $arg is a file name for 'file' or 'from', a size in
62 # megabytes for 'size'.
63 sub do_set_part {
64     my ($role, $source, $arg) = @_;
65
66     my ($p) = $parts{$role} = {};
67     if ($source eq 'file') {
68         if (read_mbr ($arg)) {
69             print STDERR "warning: $arg looks like a partitioned disk ";
70             print STDERR "(did you want --$role-from=$arg or --disk=$arg?)\n"
71         }
72
73         $p->{FILE} = $arg;
74         $p->{OFFSET} = 0;
75         $p->{BYTES} = -s $arg;
76     } elsif ($source eq 'from') {
77         my (%pt) = read_partition_table ($arg);
78         my ($sp) = $pt{$role};
79         die "$arg: does not contain \L$role\E partition\n" if !defined $sp;
80
81         $p->{FILE} = $arg;
82         $p->{OFFSET} = $sp->{START} * 512;
83         $p->{BYTES} = $sp->{SECTORS} * 512;
84     } elsif ($source eq 'size') {
85         $arg =~ /^\d+(\.\d+)?|\.\d+$/ or die "$arg: not a valid size in MB\n";
86
87         $p->{FILE} = "/dev/zero";
88         $p->{OFFSET} = 0;
89         $p->{BYTES} = ceil ($arg * 1024 * 1024);
90     } else {
91         die;
92     }
93 }
94
95 # set_geometry('HEADS,SPT')
96 # set_geometry('zip')
97 #
98 # For use as a helper function for Getopt::Long::GetOptions to set
99 # disk geometry.
100 sub set_geometry {
101     local ($_) = $_[1];
102     if ($_ eq 'zip') {
103         @geometry{'H', 'S'} = (64, 32);
104     } else {
105         @geometry{'H', 'S'} = /^(\d+)[,\s]+(\d+)$/
106           or die "bad syntax for geometry\n";
107         $geometry{H} <= 255 or die "heads limited to 255\n";
108         $geometry{S} <= 63 or die "sectors per track limited to 63\n";
109     }
110 }
111
112 # set_align('bochs|full|none')
113 #
114 # For use as a helper function for Getopt::Long::GetOptions to set
115 # partition alignment.
116 sub set_align {
117     $align = $_[1];
118     die "unknown alignment type \"$align\"\n"
119       if $align ne 'bochs' && $align ne 'full' && $align ne 'none';
120 }
121
122 # assemble_disk(%args)
123 #
124 # Creates a virtual disk $args{DISK} containing the partitions
125 # described by @args{KERNEL, FILESYS, SCRATCH, SWAP}.
126 #
127 # Required arguments:
128 #   DISK => output disk file name
129 #   HANDLE => output file handle (will be closed)
130 #
131 # Normally at least one of the following is included:
132 #   KERNEL, FILESYS, SCRATCH, SWAP => {input:
133 #                                      FILE => file to read,
134 #                                      OFFSET => byte offset in file,
135 #                                      BYTES => byte count from file,
136 #
137 #                                      output:
138 #                                      DISK => output disk file name,
139 #                                      START => sector offset in DISK,
140 #                                      SECTORS => sector count in DISK},
141 #
142 # Optional arguments:
143 #   ALIGN => 'bochs' (default), 'full', or 'none'
144 #   GEOMETRY => {H => heads, S => sectors per track} (default 16, 63)
145 #   FORMAT => 'partitioned' (default) or 'raw'
146 #   LOADER => $LOADER_SIZE-byte string containing the loader binary
147 #   ARGS => ['arg 1', 'arg 2', ...]
148 sub assemble_disk {
149     my (%args) = @_;
150
151     my (%geometry) = %{$args{GEOMETRY}};
152     $geometry{H} = 16 if !defined $geometry{H};
153     $geometry{S} = 63 if !defined $geometry{S};
154
155     my ($align);        # Align partition start, end to cylinder boundary?
156     my ($pad);          # Pad end of disk out to cylinder boundary?
157     if (!defined ($args{ALIGN}) || $args{ALIGN} eq 'bochs') {
158         $align = 0;
159         $pad = 1;
160     } elsif ($args{ALIGN} eq 'full') {
161         $align = 1;
162         $pad = 0;
163     } elsif ($args{ALIGN} eq 'none') {
164         $align = $pad = 0;
165     } else {
166         die;
167     }
168
169     my ($format) = $args{FORMAT} || 'partitioned';
170     die if $format ne 'partitioned' && $format ne 'raw';
171
172     # Check that we have apartitions to copy in.
173     my $part_cnt = grep (defined ($args{$_}), keys %role2type);
174     die "must have exactly one partition for raw output\n"
175       if $format eq 'raw' && $part_cnt != 1;
176
177     # Calculate the disk size.
178     my ($total_sectors) = 0;
179     if ($format eq 'partitioned') {
180         $total_sectors += $align ? $geometry{S} : 1;
181     }
182     for my $role (@role_order) {
183         my ($p) = $args{$role};
184         next if !defined $p;
185
186         die if $p->{DISK};
187
188         my ($bytes) = $p->{BYTES};
189         my ($start) = $total_sectors;
190         my ($end) = $start + div_round_up ($bytes, 512);
191         $end = round_up ($end, cyl_sectors (%geometry)) if $align;
192
193         $p->{DISK} = $args{DISK};
194         $p->{START} = $start;
195         $p->{SECTORS} = $end - $start;
196         $total_sectors = $end;
197     }
198
199     # Write the disk.
200     my ($disk_fn) = $args{DISK};
201     my ($disk) = $args{HANDLE};
202     if ($format eq 'partitioned') {
203         # Pack loader into MBR.
204         my ($loader) = $args{LOADER} || "\xcd\x18";
205         my ($mbr) = pack ("a$LOADER_SIZE", $loader);
206
207         $mbr .= make_kernel_command_line (@{$args{ARGS}});
208
209         # Pack partition table into MBR.
210         $mbr .= make_partition_table (\%geometry, \%args);
211
212         # Add signature to MBR.
213         $mbr .= pack ("v", 0xaa55);
214
215         die if length ($mbr) != 512;
216         write_fully ($disk, $disk_fn, $mbr);
217         write_zeros ($disk, $disk_fn, 512 * ($geometry{S} - 1)) if $align;
218     }
219     for my $role (@role_order) {
220         my ($p) = $args{$role};
221         next if !defined $p;
222
223         my ($source);
224         my ($fn) = $p->{FILE};
225         open ($source, '<', $fn) or die "$fn: open: $!\n";
226         if ($p->{OFFSET}) {
227             sysseek ($source, $p->{OFFSET}, 0) == $p->{OFFSET}
228               or die "$fn: seek: $!\n";
229         }
230         copy_file ($source, $fn, $disk, $disk_fn, $p->{BYTES});
231         close ($source) or die "$fn: close: $!\n";
232
233         write_zeros ($disk, $disk_fn, $p->{SECTORS} * 512 - $p->{BYTES});
234     }
235     if ($pad) {
236         my ($pad_sectors) = round_up ($total_sectors, cyl_sectors (%geometry));
237         write_zeros ($disk, $disk_fn, ($pad_sectors - $total_sectors) * 512);
238     }
239     close ($disk) or die "$disk: close: $!\n";
240 }
241
242 # make_partition_table({H => heads, S => sectors}, {KERNEL => ..., ...})
243 #
244 # Creates and returns a partition table for the given partitions and
245 # disk geometry.
246 sub make_partition_table {
247     my ($geometry, $partitions) = @_;
248     my ($table) = '';
249     for my $role (@role_order) {
250         defined (my $p = $partitions->{$role}) or next;
251
252         my $end = $p->{START} + $p->{SECTORS} - 1;
253         my $bootable = $role eq 'KERNEL';
254
255         $table .= pack ("C", $bootable ? 0x80 : 0);   # Bootable?
256         $table .= pack_chs ($p->{START}, $geometry);  # CHS of partition start
257         $table .= pack ("C", $role2type{$role});      # Partition type
258         $table .= pack_chs($end, $geometry);          # CHS of partition end
259         $table .= pack ("V", $p->{START});            # LBA of partition start
260         $table .= pack ("V", $p->{SECTORS});          # Length in sectors
261         die if length ($table) % 16;
262     }
263     $table = "\0" x 16 . $table while length ($table) < 64;
264     return pack ("a64", $table);
265 }
266
267 # make_kernel_command_line(@args)
268 #
269 # Returns the raw bytes to write to an MBR at offset $LOADER_SIZE to
270 # set a Pintos kernel command line.
271 sub make_kernel_command_line {
272     my (@args) = @_;
273     my ($args) = join ('', map ("$_\0", @args));
274     die "command line exceeds 128 bytes" if length ($args) > 128;
275     return pack ("V a128", scalar (@args), $args);
276 }
277
278 # copy_file($from_handle, $from_file_name, $to_handle, $to_file_name, $size)
279 #
280 # Copies $size bytes from $from_handle to $to_handle.
281 # $from_file_name and $to_file_name are used in error messages.
282 sub copy_file {
283     my ($from_handle, $from_file_name, $to_handle, $to_file_name, $size) = @_;
284
285     while ($size > 0) {
286         my ($chunk_size) = 4096;
287         $chunk_size = $size if $chunk_size > $size;
288         $size -= $chunk_size;
289
290         my ($data) = read_fully ($from_handle, $from_file_name, $chunk_size);
291         write_fully ($to_handle, $to_file_name, $data);
292     }
293 }
294
295 # read_fully($handle, $file_name, $bytes)
296 #
297 # Reads exactly $bytes bytes from $handle and returns the data read.
298 # $file_name is used in error messages.
299 sub read_fully {
300     my ($handle, $file_name, $bytes) = @_;
301     my ($data);
302     my ($read_bytes) = sysread ($handle, $data, $bytes);
303     die "$file_name: read: $!\n" if !defined $read_bytes;
304     die "$file_name: unexpected end of file\n" if $read_bytes != $bytes;
305     return $data;
306 }
307
308 # write_fully($handle, $file_name, $data)
309 #
310 # Write $data to $handle.
311 # $file_name is used in error messages.
312 sub write_fully {
313     my ($handle, $file_name, $data) = @_;
314     my ($written_bytes) = syswrite ($handle, $data);
315     die "$file_name: write: $!\n" if !defined $written_bytes;
316     die "$file_name: short write\n" if $written_bytes != length $data;
317 }
318
319 sub write_zeros {
320     my ($handle, $file_name, $size) = @_;
321
322     while ($size > 0) {
323         my ($chunk_size) = 4096;
324         $chunk_size = $size if $chunk_size > $size;
325         $size -= $chunk_size;
326
327         write_fully ($handle, $file_name, "\0" x $chunk_size);
328     }
329 }
330
331 # div_round_up($x,$y)
332 #
333 # Returns $x / $y, rounded up to the nearest integer.
334 # $y must be an integer.
335 sub div_round_up {
336     my ($x, $y) = @_;
337     return int ((ceil ($x) + $y - 1) / $y);
338 }
339
340 # round_up($x, $y)
341 #
342 # Returns $x rounded up to the nearest multiple of $y.
343 # $y must be an integer.
344 sub round_up {
345     my ($x, $y) = @_;
346     return div_round_up ($x, $y) * $y;
347 }
348
349 # cyl_sectors(H => heads, S => sectors)
350 #
351 # Returns the number of sectors in a cylinder of a disk with the given
352 # geometry.
353 sub cyl_sectors {
354     my (%geometry) = @_;
355     return $geometry{H} * $geometry{S};
356 }
357
358 # read_loader($file_name)
359 #
360 # Reads and returns the first $LOADER_SIZE bytes in $file_name.
361 # If $file_name is undefined, tries to find the default loader.
362 # Makes sure that the loader is a reasonable size.
363 sub read_loader {
364     my ($name) = @_;
365     $name = find_file ("loader.bin") if !defined $name;
366     die "Cannot find loader\n" if !defined $name;
367
368     my ($handle);
369     open ($handle, '<', $name) or die "$name: open: $!\n";
370     -s $handle == $LOADER_SIZE || -s $handle == 512
371       or die "$name: must be exactly $LOADER_SIZE or 512 bytes long\n";
372     $loader = read_fully ($handle, $name, $LOADER_SIZE);
373     close ($handle) or die "$name: close: $!\n";
374     return $loader;
375 }
376
377 # pack_chs($lba, {H => heads, S => sectors})
378 #
379 # Converts logical sector $lba to a 3-byte packed geometrical sector
380 # in the format used in PC partition tables (see [Partitions]) and
381 # returns the geometrical sector as a 3-byte string.
382 sub pack_chs {
383     my ($lba, $geometry) = @_;
384     my ($cyl, $head, $sect) = lba_to_chs ($lba, $geometry);
385     return pack ("CCC", $head, $sect | (($cyl >> 2) & 0xc0), $cyl & 0xff);
386 }
387
388 # lba_to_chs($lba, {H => heads, S => sectors})
389 #
390 # Returns the geometrical sector corresponding to logical sector $lba
391 # given the specified geometry.
392 sub lba_to_chs {
393     my ($lba, $geometry) = @_;
394     my ($hpc) = $geometry->{H};
395     my ($spt) = $geometry->{S};
396
397     # Source:
398     # http://en.wikipedia.org/wiki/CHS_conversion
399     use integer;
400     my $cyl = $lba / ($hpc * $spt);
401     my $temp = $lba % ($hpc * $spt);
402     my $head = $temp / $spt;
403     my $sect = $temp % $spt + 1;
404
405     # Source:
406     # http://www.cgsecurity.org/wiki/Intel_Partition_Table
407     if ($cyl <= 1023) {
408         return ($cyl, $head, $sect);
409     } else {
410         return (1023, 254, 63); ## or should this be (1023, $hpc, $spt)?
411     }
412 }
413
414 # read_mbr($file)
415 #
416 # Tries to read an MBR from $file.  Returns the 512-byte MBR if
417 # successful, otherwise numeric 0.
418 sub read_mbr {
419     my ($file) = @_;
420     my ($retval) = 0;
421     open (FILE, '<', $file) or die "$file: open: $!\n";
422     if (-s FILE == 0) {
423         die "$file: file has zero size\n";
424     } elsif (-s FILE >= 512) {
425         my ($mbr);
426         sysread (FILE, $mbr, 512) == 512 or die "$file: read: $!\n";
427         $retval = $mbr if unpack ("v", substr ($mbr, 510)) == 0xaa55;
428     }
429     close (FILE);
430     return $retval;
431 }
432
433 # interpret_partition_table($mbr, $disk)
434 #
435 # Parses the partition-table in the specified 512-byte $mbr and
436 # returns the partitions.  $disk is used for error messages.
437 sub interpret_partition_table {
438     my ($mbr, $disk) = @_;
439     my (%parts);
440     for my $i (0...3) {
441         my ($bootable, $valid, $type, $lba_start, $lba_length)
442           = unpack ("C X V C x3 V V", substr ($mbr, 446 + 16 * $i, 16));
443         next if !$valid;
444
445         (print STDERR "warning: invalid partition entry $i in $disk\n"),
446           next if $bootable != 0 && $bootable != 0x80;
447
448         my ($role) = $type2role{$type};
449         (printf STDERR "warning: non-Pintos partition type 0x%02x in %s\n",
450          $type, $disk),
451           next if !defined $role;
452
453         (print STDERR "warning: duplicate \L$role\E partition in $disk\n"),
454           next if exists $parts{$role};
455
456         $parts{$role} = {START => $lba_start,
457                          SECTORS => $lba_length};
458     }
459     return %parts;
460 }
461
462 # find_file($base_name)
463 #
464 # Looks for a file named $base_name in a couple of likely spots.  If
465 # found, returns the name; otherwise, returns undef.
466 sub find_file {
467     my ($base_name) = @_;
468     -e && return $_ foreach $base_name, "build/$base_name";
469     return undef;
470 }
471
472 # read_partition_table($file)
473 #
474 # Reads a partition table from $file and returns the parsed
475 # partitions.  Dies if partitions can't be read.
476 sub read_partition_table {
477     my ($file) = @_;
478     my ($mbr) = read_mbr ($file);
479     die "$file: not a partitioned disk\n" if !$mbr;
480     return interpret_partition_table ($mbr, $file);
481 }
482
483 # max(@args)
484 #
485 # Returns the numerically largest value in @args.
486 sub max {
487     my ($max) = $_[0];
488     foreach (@_[1..$#_]) {
489         $max = $_ if $_ > $max;
490     }
491     return $max;
492 }
493
494 1;