Building on Basics
Once you understand the basic concepts of C programming, the rest is pretty
easy. The bulk of the power of C comes from using other functions. In fact,
if the functions were removed from any of the preceding programs, all that
would remain are very basic statements.
0x281 File Access
There are two primary ways to access files in C: file descriptors and filestreams.
File descriptors use a set of low-level I/O functions, and filestreams are
a higher-level form of buffered I/O that is built on the lower-level functions.
Some consider the filestream functions easier to program with; however, file
descriptors are more direct. In this book, the focus will be on the low-level
I/O functions that use file descriptors.
82 0x200
The bar code on the back of this book represents a number. Because this
number is unique among the other books in a bookstore, the cashier can
scan the number at checkout and use it to reference information about this
book in the store’s database. Similarly, a file descriptor is a number that is
used to reference open files. Four common functions that use file descriptors
are open(), close(), read(), and write(). All of these functions will return −1 if
there is an error. The open() function opens a file for reading and/or writing
and returns a file descriptor. The returned file descriptor is just an integer
value, but it is unique among open files. The file descriptor is passed as an
argument to the other functions like a pointer to the opened file. For the
close() function, the file descriptor is the only argument. The read() and
write() functions’ arguments are the file descriptor, a pointer to the data to
read or write, and the number of bytes to read or write from that location.
The arguments to the open() function are a pointer to the filename to open
and a series of predefined flags that specify the access mode. These flags and
their usage will be explained in depth later, but for now let’s take a look at a
simple note-taking program that uses file descriptors—simplenote.c. This
program accepts a note as a command-line argument and then adds it to the
end of the file /tmp/notes. This program uses several functions, including a
familiar looking error-checked heap memory allocation function. Other functions
are used to display a usage message and to handle fatal errors. The
usage() function is simply defined before main(), so it doesn’t need a function
prototype.
simplenote.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
void usage(char *prog_name, char *filename) {
printf("Usage: %s <data to add to %s>\n", prog_name, filename);
exit(0);
}
void fatal(char *); // A function for fatal errors
void *ec_malloc(unsigned int); // An error-checked malloc() wrapper
int main(int argc, char *argv[]) {
int fd; // file descriptor
char *buffer, *datafile;
buffer = (char *) ec_malloc(100);
datafile = (char *) ec_malloc(20);
strcpy(datafile, "/tmp/notes");
if(argc < 2) // If there aren't command-line arguments,
usage(argv[0], datafile); // display usage message and exit.
P rogramming 83
strcpy(buffer, argv[1]); // Copy into buffer.
printf("[DEBUG] buffer @ %p: \'%s\'\n", buffer, buffer);
printf("[DEBUG] datafile @ %p: \'%s\'\n", datafile, datafile);
strncat(buffer, "\n", 1); // Add a newline on the end.
// Opening file
fd = open(datafile, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR);
if(fd == -1)
fatal("in main() while opening file");
printf("[DEBUG] file descriptor is %d\n", fd);
// Writing data
if(write(fd, buffer, strlen(buffer)) == -1)
fatal("in main() while writing buffer to file");
// Closing file
if(close(fd) == -1)
fatal("in main() while closing file");
printf("Note has been saved.\n");
free(buffer);
free(datafile);
}
// A function to display an error message and then exit
void fatal(char *message) {
char error_message[100];
strcpy(error_message, "[!!] Fatal Error ");
strncat(error_message, message, 83);
perror(error_message);
exit(-1);
}
// An error-checked malloc() wrapper function
void *ec_malloc(unsigned int size) {
void *ptr;
ptr = malloc(size);
if(ptr == NULL)
fatal("in ec_malloc() on memory allocation");
return ptr;
}
Besides the strange-looking flags used in the open() function, most of this
code should be readable. There are also a few standard functions that we
haven’t used before. The strlen() function accepts a string and returns its
length. It’s used in combination with the write() function, since it needs to
know how many bytes to write. The perror() function is short for print error and is
used in fatal() to print an additional error message (if it exists) before exiting.
reader@hacking:~/booksrc $ gcc -o simplenote simplenote.c
reader@hacking:~/booksrc $ ./simplenote
Usage: ./simplenote <data to add to /tmp/notes>
84 0x200
reader@hacking:~/booksrc $ ./simplenote "this is a test note"
[DEBUG] buffer @ 0x804a008: 'this is a test note'
[DEBUG] datafile @ 0x804a070: '/tmp/notes'
[DEBUG] file descriptor is 3
Note has been saved.
reader@hacking:~/booksrc $ cat /tmp/notes
this is a test note
reader@hacking:~/booksrc $ ./simplenote "great, it works"
[DEBUG] buffer @ 0x804a008: 'great, it works'
[DEBUG] datafile @ 0x804a070: '/tmp/notes'
[DEBUG] file descriptor is 3
Note has been saved.
reader@hacking:~/booksrc $ cat /tmp/notes
this is a test note
great, it works
reader@hacking:~/booksrc $
The output of the program’s execution is pretty self-explanatory, but
there are some things about the source code that need further explanation.
The files fcntl.h and sys/stat.h had to be included, since those files define the
flags used with the open() function. The first set of flags is found in fcntl.h
and is used to set the access mode. The access mode must use at least one of
the following three flags:
These flags can be combined with several other optional flags using the
bitwise OR operator. A few of the more common and useful of these flags are
as follows:
Bitwise operations combine bits using standard logic gates such as OR and
AND. When two bits enter an OR gate, the result is 1 if either the first bit or the
second bit is 1. If two bits enter an AND gate, the result is 1 only if both the first
bit and the second bit are 1. Full 32-bit values can use these bitwise operators to
perform logic operations on each corresponding bit. The source code of
bitwise.c and the program output demonstrate these bitwise operations.
bitwise.c
#include <stdio.h>
int main() {
int i, bit_a, bit_b;
printf("bitwise OR operator |\n");
O_RDONLY Open file for read-only access.
O_WRONLY Open file for write-only access.
O_RDWR Open file for both read and write access.
O_APPEND Write data at the end of the file.
O_TRUNC If the file already exists, truncate the file to 0 length.
O_CREAT Create the file if it doesn’t exist.
P rogramming 85
for(i=0; i < 4; i++) {
bit_a = (i & 2) / 2; // Get the second bit.
bit_b = (i & 1); // Get the first bit.
printf("%d | %d = %d\n", bit_a, bit_b, bit_a | bit_b);
}
printf("\nbitwise AND operator &\n");
for(i=0; i < 4; i++) {
bit_a = (i & 2) / 2; // Get the second bit.
bit_b = (i & 1); // Get the first bit.
printf("%d & %d = %d\n", bit_a, bit_b, bit_a & bit_b);
}
}
The results of compiling and executing bitwise.c are as follows.
reader@hacking:~/booksrc $ gcc bitwise.c
reader@hacking:~/booksrc $ ./a.out
bitwise OR operator |
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
bitwise AND operator &
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
reader@hacking:~/booksrc $
The flags used for the open() function have values that correspond to
single bits. This way, flags can be combined using OR logic without destroying
any information. The fcntl_flags.c program and its output explore some
of the flag values defined by fcntl.h and how they combine with each other.
fcntl_flags.c
#include <stdio.h>
#include <fcntl.h>
void display_flags(char *, unsigned int);
void binary_print(unsigned int);
int main(int argc, char *argv[]) {
display_flags("O_RDONLY\t\t", O_RDONLY);
display_flags("O_WRONLY\t\t", O_WRONLY);
display_flags("O_RDWR\t\t\t", O_RDWR);
printf("\n");
display_flags("O_APPEND\t\t", O_APPEND);
display_flags("O_TRUNC\t\t\t", O_TRUNC);
display_flags("O_CREAT\t\t\t", O_CREAT);
86 0x200
printf("\n");
display_flags("O_WRONLY|O_APPEND|O_CREAT", O_WRONLY|O_APPEND|O_CREAT);
}
void display_flags(char *label, unsigned int value) {
printf("%s\t: %d\t:", label, value);
binary_print(value);
printf("\n");
}
void binary_print(unsigned int value) {
unsigned int mask = 0xff000000; // Start with a mask for the highest byte.
unsigned int shift = 256*256*256; // Start with a shift for the highest byte.
unsigned int byte, byte_iterator, bit_iterator;
for(byte_iterator=0; byte_iterator < 4; byte_iterator++) {
byte = (value & mask) / shift; // Isolate each byte.
printf(" ");
for(bit_iterator=0; bit_iterator < 8; bit_iterator++) { // Print the byte's bits.
if(byte & 0x80) // If the highest bit in the byte isn't 0,
printf("1"); // print a 1.
else
printf("0"); // Otherwise, print a 0.
byte *= 2; // Move all the bits to the left by 1.
}
mask /= 256; // Move the bits in mask right by 8.
shift /= 256; // Move the bits in shift right by 8.
}
}
The results of compiling and executing fcntl_flags.c are as follows.
reader@hacking:~/booksrc $ gcc fcntl_flags.c
reader@hacking:~/booksrc $ ./a.out
O_RDONLY : 0 : 00000000 00000000 00000000 00000000
O_WRONLY : 1 : 00000000 00000000 00000000 00000001
O_RDWR : 2 : 00000000 00000000 00000000 00000010
O_APPEND : 1024 : 00000000 00000000 00000100 00000000
O_TRUNC : 512 : 00000000 00000000 00000010 00000000
O_CREAT : 64 : 00000000 00000000 00000000 01000000
O_WRONLY|O_APPEND|O_CREAT : 1089 : 00000000 00000000 00000100 01000001
$
Using bit flags in combination with bitwise logic is an efficient and commonly
used technique. As long as each flag is a number that only has unique
bits turned on, the effect of doing a bitwise OR on these values is the same as
adding them. In fcntl_flags.c, 1 + 1024 + 64 = 1089. This technique only works
when all the bits are unique, though.
Once you understand the basic concepts of C programming, the rest is pretty
easy. The bulk of the power of C comes from using other functions. In fact,
if the functions were removed from any of the preceding programs, all that
would remain are very basic statements.
0x281 File Access
There are two primary ways to access files in C: file descriptors and filestreams.
File descriptors use a set of low-level I/O functions, and filestreams are
a higher-level form of buffered I/O that is built on the lower-level functions.
Some consider the filestream functions easier to program with; however, file
descriptors are more direct. In this book, the focus will be on the low-level
I/O functions that use file descriptors.
82 0x200
The bar code on the back of this book represents a number. Because this
number is unique among the other books in a bookstore, the cashier can
scan the number at checkout and use it to reference information about this
book in the store’s database. Similarly, a file descriptor is a number that is
used to reference open files. Four common functions that use file descriptors
are open(), close(), read(), and write(). All of these functions will return −1 if
there is an error. The open() function opens a file for reading and/or writing
and returns a file descriptor. The returned file descriptor is just an integer
value, but it is unique among open files. The file descriptor is passed as an
argument to the other functions like a pointer to the opened file. For the
close() function, the file descriptor is the only argument. The read() and
write() functions’ arguments are the file descriptor, a pointer to the data to
read or write, and the number of bytes to read or write from that location.
The arguments to the open() function are a pointer to the filename to open
and a series of predefined flags that specify the access mode. These flags and
their usage will be explained in depth later, but for now let’s take a look at a
simple note-taking program that uses file descriptors—simplenote.c. This
program accepts a note as a command-line argument and then adds it to the
end of the file /tmp/notes. This program uses several functions, including a
familiar looking error-checked heap memory allocation function. Other functions
are used to display a usage message and to handle fatal errors. The
usage() function is simply defined before main(), so it doesn’t need a function
prototype.
simplenote.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
void usage(char *prog_name, char *filename) {
printf("Usage: %s <data to add to %s>\n", prog_name, filename);
exit(0);
}
void fatal(char *); // A function for fatal errors
void *ec_malloc(unsigned int); // An error-checked malloc() wrapper
int main(int argc, char *argv[]) {
int fd; // file descriptor
char *buffer, *datafile;
buffer = (char *) ec_malloc(100);
datafile = (char *) ec_malloc(20);
strcpy(datafile, "/tmp/notes");
if(argc < 2) // If there aren't command-line arguments,
usage(argv[0], datafile); // display usage message and exit.
P rogramming 83
strcpy(buffer, argv[1]); // Copy into buffer.
printf("[DEBUG] buffer @ %p: \'%s\'\n", buffer, buffer);
printf("[DEBUG] datafile @ %p: \'%s\'\n", datafile, datafile);
strncat(buffer, "\n", 1); // Add a newline on the end.
// Opening file
fd = open(datafile, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR);
if(fd == -1)
fatal("in main() while opening file");
printf("[DEBUG] file descriptor is %d\n", fd);
// Writing data
if(write(fd, buffer, strlen(buffer)) == -1)
fatal("in main() while writing buffer to file");
// Closing file
if(close(fd) == -1)
fatal("in main() while closing file");
printf("Note has been saved.\n");
free(buffer);
free(datafile);
}
// A function to display an error message and then exit
void fatal(char *message) {
char error_message[100];
strcpy(error_message, "[!!] Fatal Error ");
strncat(error_message, message, 83);
perror(error_message);
exit(-1);
}
// An error-checked malloc() wrapper function
void *ec_malloc(unsigned int size) {
void *ptr;
ptr = malloc(size);
if(ptr == NULL)
fatal("in ec_malloc() on memory allocation");
return ptr;
}
Besides the strange-looking flags used in the open() function, most of this
code should be readable. There are also a few standard functions that we
haven’t used before. The strlen() function accepts a string and returns its
length. It’s used in combination with the write() function, since it needs to
know how many bytes to write. The perror() function is short for print error and is
used in fatal() to print an additional error message (if it exists) before exiting.
reader@hacking:~/booksrc $ gcc -o simplenote simplenote.c
reader@hacking:~/booksrc $ ./simplenote
Usage: ./simplenote <data to add to /tmp/notes>
84 0x200
reader@hacking:~/booksrc $ ./simplenote "this is a test note"
[DEBUG] buffer @ 0x804a008: 'this is a test note'
[DEBUG] datafile @ 0x804a070: '/tmp/notes'
[DEBUG] file descriptor is 3
Note has been saved.
reader@hacking:~/booksrc $ cat /tmp/notes
this is a test note
reader@hacking:~/booksrc $ ./simplenote "great, it works"
[DEBUG] buffer @ 0x804a008: 'great, it works'
[DEBUG] datafile @ 0x804a070: '/tmp/notes'
[DEBUG] file descriptor is 3
Note has been saved.
reader@hacking:~/booksrc $ cat /tmp/notes
this is a test note
great, it works
reader@hacking:~/booksrc $
The output of the program’s execution is pretty self-explanatory, but
there are some things about the source code that need further explanation.
The files fcntl.h and sys/stat.h had to be included, since those files define the
flags used with the open() function. The first set of flags is found in fcntl.h
and is used to set the access mode. The access mode must use at least one of
the following three flags:
These flags can be combined with several other optional flags using the
bitwise OR operator. A few of the more common and useful of these flags are
as follows:
Bitwise operations combine bits using standard logic gates such as OR and
AND. When two bits enter an OR gate, the result is 1 if either the first bit or the
second bit is 1. If two bits enter an AND gate, the result is 1 only if both the first
bit and the second bit are 1. Full 32-bit values can use these bitwise operators to
perform logic operations on each corresponding bit. The source code of
bitwise.c and the program output demonstrate these bitwise operations.
bitwise.c
#include <stdio.h>
int main() {
int i, bit_a, bit_b;
printf("bitwise OR operator |\n");
O_RDONLY Open file for read-only access.
O_WRONLY Open file for write-only access.
O_RDWR Open file for both read and write access.
O_APPEND Write data at the end of the file.
O_TRUNC If the file already exists, truncate the file to 0 length.
O_CREAT Create the file if it doesn’t exist.
P rogramming 85
for(i=0; i < 4; i++) {
bit_a = (i & 2) / 2; // Get the second bit.
bit_b = (i & 1); // Get the first bit.
printf("%d | %d = %d\n", bit_a, bit_b, bit_a | bit_b);
}
printf("\nbitwise AND operator &\n");
for(i=0; i < 4; i++) {
bit_a = (i & 2) / 2; // Get the second bit.
bit_b = (i & 1); // Get the first bit.
printf("%d & %d = %d\n", bit_a, bit_b, bit_a & bit_b);
}
}
The results of compiling and executing bitwise.c are as follows.
reader@hacking:~/booksrc $ gcc bitwise.c
reader@hacking:~/booksrc $ ./a.out
bitwise OR operator |
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
bitwise AND operator &
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
reader@hacking:~/booksrc $
The flags used for the open() function have values that correspond to
single bits. This way, flags can be combined using OR logic without destroying
any information. The fcntl_flags.c program and its output explore some
of the flag values defined by fcntl.h and how they combine with each other.
fcntl_flags.c
#include <stdio.h>
#include <fcntl.h>
void display_flags(char *, unsigned int);
void binary_print(unsigned int);
int main(int argc, char *argv[]) {
display_flags("O_RDONLY\t\t", O_RDONLY);
display_flags("O_WRONLY\t\t", O_WRONLY);
display_flags("O_RDWR\t\t\t", O_RDWR);
printf("\n");
display_flags("O_APPEND\t\t", O_APPEND);
display_flags("O_TRUNC\t\t\t", O_TRUNC);
display_flags("O_CREAT\t\t\t", O_CREAT);
86 0x200
printf("\n");
display_flags("O_WRONLY|O_APPEND|O_CREAT", O_WRONLY|O_APPEND|O_CREAT);
}
void display_flags(char *label, unsigned int value) {
printf("%s\t: %d\t:", label, value);
binary_print(value);
printf("\n");
}
void binary_print(unsigned int value) {
unsigned int mask = 0xff000000; // Start with a mask for the highest byte.
unsigned int shift = 256*256*256; // Start with a shift for the highest byte.
unsigned int byte, byte_iterator, bit_iterator;
for(byte_iterator=0; byte_iterator < 4; byte_iterator++) {
byte = (value & mask) / shift; // Isolate each byte.
printf(" ");
for(bit_iterator=0; bit_iterator < 8; bit_iterator++) { // Print the byte's bits.
if(byte & 0x80) // If the highest bit in the byte isn't 0,
printf("1"); // print a 1.
else
printf("0"); // Otherwise, print a 0.
byte *= 2; // Move all the bits to the left by 1.
}
mask /= 256; // Move the bits in mask right by 8.
shift /= 256; // Move the bits in shift right by 8.
}
}
The results of compiling and executing fcntl_flags.c are as follows.
reader@hacking:~/booksrc $ gcc fcntl_flags.c
reader@hacking:~/booksrc $ ./a.out
O_RDONLY : 0 : 00000000 00000000 00000000 00000000
O_WRONLY : 1 : 00000000 00000000 00000000 00000001
O_RDWR : 2 : 00000000 00000000 00000000 00000010
O_APPEND : 1024 : 00000000 00000000 00000100 00000000
O_TRUNC : 512 : 00000000 00000000 00000010 00000000
O_CREAT : 64 : 00000000 00000000 00000000 01000000
O_WRONLY|O_APPEND|O_CREAT : 1089 : 00000000 00000000 00000100 01000001
$
Using bit flags in combination with bitwise logic is an efficient and commonly
used technique. As long as each flag is a number that only has unique
bits turned on, the effect of doing a bitwise OR on these values is the same as
adding them. In fcntl_flags.c, 1 + 1024 + 64 = 1089. This technique only works
when all the bits are unique, though.
0 comments:
Post a Comment