I’ve been doing some Linux kernel development in my spare time over the past 6 months or so. The goal has been to get my old Chumby 8 (stock kernel 2.6.28) running on a modern kernel with custom firmware. It has been going really well and there have been lots of fun problems I’ve needed to solve along the way. I may write some posts about that process if there is any interest. It’s been a blast.

Anyway, I thought it would also be fun to jump into the current craze of OpenAI’s ChatGPT free research preview and apply it to Linux kernel development. Let’s see just how well this AI can write a basic kernel module that I describe. Can it improve the module as I ask it to make tweaks?

I started out by asking:

Write a Linux kernel module that prints “Hello world” to the console every 5 seconds. Also provide a Makefile for compiling it.

ChatGPT dutifully responded with a basic kernel module and Makefile:

Here is an example kernel module that prints “Hello world” to the console every 5 seconds:

#include <linux/module.h>  // Needed for all kernel modules
#include <linux/kernel.h>  // Needed for KERN_INFO
#include <linux/init.h>    // Needed for the macros
#include <linux/jiffies.h> // Needed for jiffies
#include <linux/delay.h>   // Needed for msleep

static unsigned long jiffies_at_load;

static int __init hello_world_init(void)
{
    jiffies_at_load = jiffies;
    printk(KERN_INFO "Hello world!\n");
    return 0;
}

static void __exit hello_world_exit(void)
{
    printk(KERN_INFO "Goodbye world!\n");
}

module_init(hello_world_init);
module_exit(hello_world_exit);

static void print_hello_world(void)
{
    // Calculate the time that has passed since the module was loaded
    unsigned long time_since_load = jiffies - jiffies_at_load;
    // Convert time in jiffies to time in seconds
    unsigned long time_since_load_sec = time_since_load / HZ;
    // If 5 seconds have passed, print "Hello world"
    if (time_since_load_sec >= 5)
    {
        printk(KERN_INFO "Hello world!\n");
        // Reset the jiffies counter so that we only print every 5 seconds
        jiffies_at_load = jiffies;
    }
    // Sleep for 1 ms to avoid hogging the CPU
    msleep(1);
}

// This function is called repeatedly by a kernel thread
static int thread_fn(void *data)
{
    while (!kthread_should_stop())
    {
        print_hello_world();
    }
    return 0;
}

// Define the kernel thread
static struct task_struct *thread;

static int __init hello_world_init(void)
{
    // Start the kernel thread
    thread = kthread_run(thread_fn, NULL, "hello_world_thread");
    return 0;
}

static void __exit hello_world_exit(void)
{
    // Stop the kernel thread
    if (thread)
    {
        kthread_stop(thread);
        thread = NULL;
    }
}

module_init(hello_world_init);
module_exit(hello_world_exit);

Here is a Makefile that can be used to compile the kernel module:

obj-m += hello_world.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

To use this Makefile, save the code above in a file named hello_world.c, and save the Makefile in the same directory. Then, run make to compile the kernel module. The

At this point, I guess the response was too long so it got cut off. I’ve noticed that ChatGPT does that sometimes. If you care, you can often get it to spit out the rest of the response if you ask it the right way. Anyway, at first glance you can tell this output has some issues. It repeats module_init() and module_exit() twice. It’s like two separate module examples were concatenated together. Despite the mistakes, there is indeed an algorithm present for printing a message every 5 seconds. I played dumb and tried to see how far it could lead me along. I ran make, which gave me this error:

Makefile:4: *** missing separator. Stop.

This was caused because ChatGPT indented the Makefile with spaces instead of tabs. In past interactions I had given this error directly to ChatGPT and it corrected it for me, so I’m confident it would have solved it for me. This time I went straight to the point instead:

Please use tabs instead of spaces when indenting the Makefile.

Here is an updated version of the Makefile that uses tabs for indentation instead of spaces:

[The same response as above, but with tabs instead of spaces]

To use this Makefile, save the code for the kernel module in a file named hello_world.c, and save the Makefile in the same directory. Then, run make to compile the kernel module. The compiled module will be named hello_world.ko. To load the module, use the insmod command:

insmod hello_world.ko

To unload the module, use the rmmod command:

rmmod hello_world

You can also use the modprobe command to both load and unload the module:

modprobe hello_world

This command will load the module if it is not already loaded, and unload the module if it is already loaded.

Huh, that’s funny. That’s absolutely not how modprobe works at all. First of all, it’s not going to find a random module I just compiled if it’s not in the correct place in /lib/modules. Plus, it doesn’t automatically remove the module if it’s already loaded. I wonder how it came up with that idea? Anyway, I tried building the module again by running make. This time, I got a bunch of compilation errors, so I told ChatGPT:

The code you provided doesn’t compile. Can you fix it?

I apologize if the code I provided does not compile. The code I provided was only an example, and it may have contained errors.

Here is a revised version of the kernel module that should compile correctly:

#include <linux/module.h>  // Needed for all kernel modules
#include <linux/kernel.h>  // Needed for KERN_INFO
#include <linux/init.h>    // Needed for the macros
#include <linux/jiffies.h> // Needed for jiffies
#include <linux/delay.h>   // Needed for msleep

static unsigned long jiffies_at_load;

// This function is called repeatedly by a kernel thread
static int thread_fn(void *data)
{
    while (!kthread_should_stop())
    {
        // Calculate the time that has passed since the module was loaded
        unsigned long time_since_load = jiffies - jiffies_at_load;
        // Convert time in jiffies to time in seconds
        unsigned long time_since_load_sec = time_since_load / HZ;
        // If 5 seconds have passed, print "Hello world"
        if (time_since_load_sec >= 5)
        {
            printk(KERN_INFO "Hello world!\n");
            // Reset the jiffies counter so that we only print every 5 seconds
            jiffies_at_load = jiffies;
        }
        // Sleep for 1 ms to avoid hogging the CPU
        msleep(1);
    }
    return 0;
}

// Define the kernel thread
static struct task_struct *thread;

static int __init hello_world_init(void)
{
    jiffies_at_load = jiffies;
    printk(KERN_INFO "Hello world!\n");
    // Start the kernel thread
    thread = kthread_run(thread_fn, NULL, "hello_world_thread");
    return 0;
}

static void __exit hello_world_exit(void)
{
    // Stop the kernel thread
    if (thread)
    {
        kthread_stop(thread);
        thread = NULL;
    }
    printk(KERN_INFO "Goodbye world!\n");
}

module_init(hello_world_init);
module_exit(hello_log_exit);

And here is a revised version of the Makefile that should work correctly:

[Omitted the Makefile and compilation instructions below because it’s exactly the same as the previous response]

Okay, that’s closer to what I’m looking for! For some reason, it decided to pass hello_log_exit instead of hello_world_exit as the argument to module_exit. Also, it’s missing an include for kthread-related stuff. So I told it about the next GCC error that I got while running make:

It says kthread_should_stop is implicitly declared.

I apologize if the code I provided does not compile. The kthread_should_stop function is declared in the linux/kthread.h header file, which must be included in the kernel module. Here is an updated version of the kernel module that includes this header file and should compile correctly:

[ChatGPT responded with another copy of hello_world.c, this time adding “#include <linux/kthread.h>” and also randomly fixing the hello_log_exit mistake from the previous version even though I didn’t ask it to. It also gave me the Makefile and build instructions again]

Once again I ran make, and this time I got another error and asked ChatGPT to fix it for me:

It’s telling me that it’s missing a MODULE_LICENSE.

In order for a kernel module to be compiled successfully, it must include a MODULE_LICENSE macro that specifies the license under which the module is released. Here is an updated version of the kernel module that includes the MODULE_LICENSE macro and should compile correctly:

[ChatGPT responded again, this time adding the following two lines to the module’s source code and again repeating the Makefile and build instructions:]

// Specify the license under which the module is released
MODULE_LICENSE("GPL");

I didn’t ask it what license to use, but I guess it decided that it should be GPL. At this point, the module actually built and I was able to insmod it to test it, and it worked!

Dec  5 22:46:39 doug-ryzen-5700G kernel: [ 4886.052329] Hello world!
Dec  5 22:46:44 doug-ryzen-5700G kernel: [ 4891.060296] Hello world!
Dec  5 22:46:49 doug-ryzen-5700G kernel: [ 4896.068377] Hello world!
Dec  5 22:46:49 doug-ryzen-5700G kernel: [ 4896.260383] Goodbye world!

I wanted it to be a little smarter about how it implements the “every 5 seconds” delay though. It seems pretty silly to implement it by sleeping in 1 ms increments.

Can you change this so it’s not so wasteful of CPU time in thread_fn?

Here is an updated version of the kernel module that uses the schedule_timeout function to sleep for a specified amount of time instead of using msleep. This should make the kernel module less wasteful of CPU time:

[It gave me an incomplete version of the code, changed to use schedule_timeout(HZ) instead of msleep(1), with #include <linux/sched.h> instead of #include <linux/delay.h>]

I don’t want to spend too much time looking at every incremental version of the module it gave me, but I still wasn’t happy here because it was trying to sleep every second instead. So I asked it outright to wait every 5 seconds:

Can’t the thread just sleep for 5 seconds each time instead?

Here is an updated version of the kernel module that uses the schedule_timeout function to sleep for 5 seconds each time instead of using a busy-wait loop. This should make the kernel module less wasteful of CPU time:

[Stripping out the rest of the response, here’s what it gave me for thread_fn:]

// This function is called repeatedly by a kernel thread
static int thread_fn(void *data)
{
    while (!kthread_should_stop())
    {
        printk(KERN_INFO "Hello world!\n");
        // Sleep for 5 seconds to avoid hogging the CPU
        schedule_timeout(HZ * 5);
    }
    return 0;
}

Nice! Except…when I tested it, it wasn’t actually sleeping for 5 seconds. It was flooding the console instead. So I complained to ChatGPT about it.

Are you sure schedule_timeout does what you think it does? It doesn’t sleep. Shouldn’t you be sleeping the thread instead?

You are correct, schedule_timeout does not cause the kernel thread to sleep. Instead, it causes the kernel to stop running the thread for a specified amount of time and to schedule another thread to run. This has the same effect as sleeping, but it allows the kernel to make more efficient use of the CPU.

Here is an updated version of the kernel module that uses the set_current_state and schedule functions to sleep the thread for 5 seconds each time:

[Stripping out the rest of the response, here is the new thread_fn:]

// This function is called repeatedly by a kernel thread
static int thread_fn(void *data)
{
    while (!kthread_should_stop())
    {
        printk(KERN_INFO "Hello world!\n");
        // Sleep for 5 seconds to avoid hogging the CPU
        set_current_state(TASK_INTERRUPTIBLE);
        schedule_timeout(HZ * 5);
    }
    return 0;
}

To be completely honest, I don’t know enough about the kernel to fully understand how schedule_timeout works or if this is actually the “right” way to do it; I probably would have continued using a sleep function instead. Either way, it does seem to work. The module would print out the message every 5 seconds. I didn’t notice until afterward that it prints the first “Hello world!” twice or I would have complained to the AI about that.

At this point I decided to start asking it to do some more stuff that I would consider boring boilerplate code. I wasn’t really expecting it to succeed:

Now, can you make the 5 seconds configurable through a module parameter instead?

Here is an updated version of the kernel module that allows the user to specify the number of seconds to sleep between printing “Hello world” using a module parameter:

[Here is the relevant excerpt of what changed in its response:]

// Define a module parameter for the number of seconds to sleep
static unsigned int sleep_sec = 5;
module_param(sleep_sec, uint, 0644);

// Define the kernel thread
static struct task_struct *thread;

// This function is called repeatedly by a kernel thread
static int thread_fn(void *data)
{
    while (!kthread_should_stop())
    {
        printk(KERN_INFO "Hello world!\n");
        // Sleep for the specified number of seconds to avoid hogging the CPU
        set_current_state(TASK_INTERRUPTIBLE);
        schedule_timeout(HZ * sleep_sec);
    }
    return 0;
}

[It also told me how to use the new parameter with insmod:]

To load the module and specify the number of seconds to sleep, use the insmod command followed by the sleep_sec module parameter and the desired value:

insmod hello_world.ko sleep_sec=10

This will load the module and cause it to sleep for 10 seconds between printing “Hello world”.

Wow! This change worked perfectly on the first try.

For a final challenge, I asked it to do one more boilerplate thing. I have no idea how to do this off the top of my head and I would have needed to look it up.

One last thing. Can you add a file in /proc called “print_count” that provides the number of times that “Hello world” has been printed to the console?

Here is an updated version of the kernel module that adds a file in the /proc filesystem called print_count that provides the number of times that “Hello world” has been printed to the console:

[Long story short, the output it gave me resulted in a compilation error because it was using struct file_operations instead of struct proc_ops which is how newer kernel versions do it]

This doesn’t compile because proc_create expects a “const struct proc_ops *” for argument 4.

You are correct, the fourth argument to proc_create should be a pointer to a struct proc_ops instead of a struct file_operations. Here is an updated version of the kernel module that uses the correct type for this argument:

#include <linux/module.h>  // Needed for all kernel modules
#include <linux/kernel.h>  // Needed for KERN_INFO
#include <linux/init.h>    // Needed for the macros
#include <linux/jiffies.h> // Needed for jiffies
#include <linux/sched.h>   // Needed for set_current_state and schedule
#include <linux/kthread.h> // Needed for kthread_should_stop
#include <linux/proc_fs.h> // Needed for the proc filesystem
#include <linux/seq_file.h> // Needed for seq_printf
#include <linux/uaccess.h> // Needed for copy_from_user and copy_to_user

// Define a module parameter for the number of seconds to sleep
static unsigned int sleep_sec = 5;
module_param(sleep_sec, uint, 0644);

// Define a counter for the number of times "Hello world" has been printed
static unsigned int print_count;

// Define the kernel thread
static struct task_struct *thread;

// This function is called repeatedly by a kernel thread
static int thread_fn(void *data)
{
    while (!kthread_should_stop())
    {
        printk(KERN_INFO "Hello world!\n");
        // Increment the print count
        print_count++;
        // Sleep for the specified number of seconds to avoid hogging the CPU
        set_current_state(TASK_INTERRUPTIBLE);
        schedule_timeout(HZ * sleep_sec);
    }
    return 0;
}

// This function is called when the /proc file is read
static int print_count_show(struct seq_file *m, void *v)
{
    seq_printf(m, "%u\n", print_count);
    return 0;
}

// This structure maps the /proc file read function to the file name
static int print_count_open(struct inode *inode, struct file *file)
{
    return single_open(file, print_count_show, NULL);
}

static const struct proc_ops print_count_fops = {
    .proc_open = print_count_open,
    .proc_read = seq_read,
    .proc_lseek = seq_lseek,
    .proc_release = single_release,
};

static int __init hello_world_init(void)
{
    printk(KERN_INFO "Hello world!\n");
    // Create the /proc file
    proc_create("print_count", 0, NULL, &print_count_fops);
    // Start the kernel thread
    thread = kthread_run(thread_fn, NULL, "hello_world_thread");
    return 0;
}

static void __exit hello_world_exit(void)
{
    // Stop the kernel thread
    if (thread)
    {
        kthread_stop(thread);
        thread = NULL;
    }
    // Remove the /proc file
    remove_proc_entry("print_count", NULL);
    printk(KERN_INFO "Goodbye world!\n");
}

// Specify the license under which the module is released
MODULE_LICENSE("GPL");

module_init(hello_world_init);
module_exit(hello_world_exit);

[I did have to ask it to give me this output in two chunks because it was getting cut off early. It also responded with a bunch of the usual usage information, including a new section about the /proc file:]

To view the number of times that “Hello world” has been printed, you can read the /proc/print_count file. For example:

cat /proc/print_count

This will print the number of times that “Hello world” has been printed to the console.

This is the point where I stopped. This driver behaves exactly how I requested. I’ve long been a believer that AI isn’t going to replace developers, but this is blowing my mind.

Did it give me correct code immediately? No, I definitely had to go through a review process with it before it gave me working code. Some of the mistakes it made were pretty boneheaded. Is the code 100% perfect? No, not by any means. But it can help me learn how to accomplish things in the kernel, likely faster than I could have looked it up myself.

It’s half amazing and half terrifying that I can convince an AI to write code that does something for me without fully understanding it. Is this the future? Will I actually be formulating AI prompts rather than writing code myself? I don’t know what to think. I feel like there still has to be human oversight (where the heck did it learn how to use modprobe and why is it so wrong?) but at a minimum this is going to speed up human developers.

Oh…and where does the GPL fit in with all of this? Is this AI spitting out chunks of code taken from other GPL sources? What if I asked it to license it as BSD instead? Would this be a way of accomplishing “license laundering”? I don’t know. That’s a whole different discussion that I probably shouldn’t get into.

I don’t really know how to end this. I’m really impressed, but the way that it confidently tells me the wrong answer at times is a little off-putting. To be fair, I’ve seen people answer questions the same way in all aspects of life…

Trackback

4 comments

  1. […] Doug Brown ☛ Getting ChatGPT to write a Linux kernel module for me […]

  2. Thanks for posting this- it’s deeply fascinating, and a bit scary.

    As to your question about the Chumby kernel project- I’d be interested in reading some blogs about that (that’s a really old kernel, so I’d love to hear how you slowly drag that forward to something approaching a modern kernel).

  3. Thanks Steve! I’ll definitely write some posts about my Chumby kernel project.

  4. Bob Jackson @ 2023-01-06 08:10

    Iteresting, I wanted to know how a chatgpt experience looks like for a person like me not familiar at all with this topic, Im a developer but dont understand anything at all about this topic. I wanted to see if a non developer can use chatgpt to develop software by themselves just by giving instructions, and to me it seems you need to have at least some general knowledge about the topic to have an understanding of what next prompt to give. This might improve in the future though, but right now If I was asked to do this very same task it would take maybe around3 days with chatgpt for me, because first I would need to become familiar with some kernel concepts, etc. Interesting.

Add your comment now