[PATCH v2] scripts: checkstack: Convert to Python
From: Breno Rodrigues Alves
Date: Mon Oct 19 2026 - 09:58:42 EST
Convert checkstack.pl to Python to unify kernel tooling in a single
language. This reduces the maintenance burden of dealing with two
different sets of idioms for the same class of scripts.
The conversion introduces a robust parse_size helper to handle dynamic
stack adjustments (e.g., sub %eax, %esp). In Perl, hex() silently
evaluates non-numeric strings to 0; Python's int() raises a ValueError.
The new helper ensures 1:1 behavioral parity without crashing on valid
objdump output.
Tested by comparing the output of checkstack.pl and checkstack.py
against a full objdump of /bin/bash on x86_64; the outputs were identical.
Assisted-by: DeepSeek
Signed-off-by: Breno Rodrigues Alves <breno3011alves@xxxxxxxxx>
---
scripts/checkstack.pl | 208 ------------------------------------------
scripts/checkstack.py | 155 +++++++++++++++++++++++++++++++
2 files changed, 155 insertions(+), 208 deletions(-)
delete mode 100755 scripts/checkstack.pl
create mode 100755 scripts/checkstack.py
diff --git a/scripts/checkstack.pl b/scripts/checkstack.pl
deleted file mode 100755
index 14ce31f73..000000000
--- a/scripts/checkstack.pl
+++ /dev/null
@@ -1,208 +0,0 @@
-#!/usr/bin/env perl
-# SPDX-License-Identifier: GPL-2.0
-
-# Check the stack usage of functions
-#
-# Copyright Joern Engel <joern@xxxxxxxxxxxxxxx>
-# Inspired by Linus Torvalds
-# Original idea maybe from Keith Owens
-# s390 port and big speedup by Arnd Bergmann <arnd@xxxxxxxxxxxxxxxxxxxx>
-# Mips port by Juan Quintela <quintela@xxxxxxxxxxxxxxxx>
-# Arm port by Holger Schurig
-# Random bits by Matt Mackall <mpm@xxxxxxxxxxx>
-# M68k port by Geert Uytterhoeven and Andreas Schwab
-# AArch64, PARISC ports by Kyle McMartin
-# sparc port by Martin Habets <errandir_news@xxxxxxxxxxxxxxxxx>
-# ppc64le port by Breno Leitao <leitao@xxxxxxxxxx>
-# riscv port by Wadim Mueller <wafgo01@xxxxxxxxx>
-# loongarch port by Youling Tang <tangyouling@xxxxxxxxxx>
-#
-# Usage:
-# objdump -d vmlinux | scripts/checkstack.pl [arch] [min_stack]
-#
-# TODO : Port to all architectures (one regex per arch)
-
-use strict;
-
-# check for arch
-#
-# $re is used for two matches:
-# $& (whole re) matches the complete objdump line with the stack growth
-# $1 (first bracket) matches the size of the stack growth
-#
-# $dre is similar, but for dynamic stack redutions:
-# $& (whole re) matches the complete objdump line with the stack growth
-# $1 (first bracket) matches the dynamic amount of the stack growth
-#
-# $sub: subroutine for special handling to check stack usage.
-#
-# use anything else and feel the pain ;)
-my (@stack, $re, $dre, $sub, $x, $xs, $funcre, $min_stack);
-{
- my $arch = shift;
- if ($arch eq "") {
- $arch = `uname -m`;
- chomp($arch);
- }
-
- $min_stack = shift;
- if ($min_stack eq "" || $min_stack !~ /^\d+$/) {
- $min_stack = 512;
- }
-
- $x = "[0-9a-f]"; # hex character
- $xs = "[0-9a-f ]"; # hex character or space
- $funcre = qr/^$x* <(.*)>:$/;
- if ($arch =~ '^(aarch|arm)64$') {
- #ffffffc0006325cc: a9bb7bfd stp x29, x30, [sp, #-80]!
- #a110: d11643ff sub sp, sp, #0x590
- $re = qr/^.*stp.*sp, ?\#-([0-9]{1,8})\]\!/o;
- $dre = qr/^.*sub.*sp, sp, #(0x$x{1,8})/o;
- } elsif ($arch eq 'arm') {
- #c0008ffc: e24dd064 sub sp, sp, #100 ; 0x64
- $re = qr/.*sub.*sp, sp, #([0-9]{1,4})/o;
- $sub = \&arm_push_handling;
- } elsif ($arch =~ /^x86(_64)?$/ || $arch =~ /^i[3456]86$/) {
- #c0105234: 81 ec ac 05 00 00 sub $0x5ac,%esp
- # or
- # 2f60: 48 81 ec e8 05 00 00 sub $0x5e8,%rsp
- $re = qr/^.*[as][du][db] \$(0x$x{1,8}),\%(e|r)sp$/o;
- $dre = qr/^.*[as][du][db] (%.*),\%(e|r)sp$/o;
- } elsif ($arch eq 'm68k') {
- # 2b6c: 4e56 fb70 linkw %fp,#-1168
- # 1df770: defc ffe4 addaw #-28,%sp
- $re = qr/.*(?:linkw %fp,|addaw )#-([0-9]{1,4})(?:,%sp)?$/o;
- } elsif ($arch eq 'mips64') {
- #8800402c: 67bdfff0 daddiu sp,sp,-16
- $re = qr/.*daddiu.*sp,sp,-([0-9]{1,8})/o;
- } elsif ($arch eq 'mips') {
- #88003254: 27bdffe0 addiu sp,sp,-32
- $re = qr/.*addiu.*sp,sp,-([0-9]{1,8})/o;
- } elsif ($arch eq 'nios2') {
- #25a8: defffb04 addi sp,sp,-20
- $re = qr/.*addi.*sp,sp,-([0-9]{1,8})/o;
- } elsif ($arch eq 'openrisc') {
- # c000043c: 9c 21 fe f0 l.addi r1,r1,-272
- $re = qr/.*l\.addi.*r1,r1,-([0-9]{1,8})/o;
- } elsif ($arch eq 'parisc' || $arch eq 'parisc64') {
- $re = qr/.*ldo ($x{1,8})\(sp\),sp/o;
- } elsif ($arch eq 'powerpc' || $arch =~ /^ppc(64)?(le)?$/ ) {
- # powerpc : 94 21 ff 30 stwu r1,-208(r1)
- # ppc64(le) : 81 ff 21 f8 stdu r1,-128(r1)
- $re = qr/.*st[dw]u.*r1,-($x{1,8})\(r1\)/o;
- } elsif ($arch =~ /^s390x?$/) {
- # 11160: a7 fb ff 60 aghi %r15,-160
- # or
- # 100092: e3 f0 ff c8 ff 71 lay %r15,-56(%r15)
- $re = qr/.*(?:lay|ag?hi).*\%r15,-([0-9]+)(?:\(\%r15\))?$/o;
- } elsif ($arch eq 'sparc' || $arch eq 'sparc64') {
- # f0019d10: 9d e3 bf 90 save %sp, -112, %sp
- $re = qr/.*save.*%sp, -([0-9]{1,8}), %sp/o;
- } elsif ($arch =~ /^riscv(64)?$/) {
- #ffffffff8036e868: c2010113 addi sp,sp,-992
- $re = qr/.*addi.*sp,sp,-([0-9]{1,8})/o;
- } elsif ($arch =~ /^loongarch(32|64)?$/) {
- #9000000000224708: 02ff4063 addi.d $sp, $sp, -48(0xfd0)
- $re = qr/.*addi\..*sp, .*sp, -([0-9]{1,8}).*/o;
- } else {
- print("wrong or unknown architecture \"$arch\"\n");
- exit
- }
-}
-
-#
-# To count stack usage of push {*, fp, ip, lr, pc} instruction in ARM,
-# if FRAME POINTER is enabled.
-# e.g. c01f0d48: e92ddff0 push {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}
-#
-sub arm_push_handling {
- my $regex = qr/.*push.*fp, ip, lr, pc}/o;
- my $size = 0;
- my $line_arg = shift;
-
- if ($line_arg =~ m/$regex/) {
- $size = $line_arg =~ tr/,//;
- $size = ($size + 1) * 4;
- }
-
- return $size;
-}
-
-#
-# main()
-#
-my ($func, $file, $lastslash, $total_size, $addr, $intro);
-
-$total_size = 0;
-
-while (my $line = <STDIN>) {
- if ($line =~ m/$funcre/) {
- $func = $1;
- next if $line !~ m/^($x*)/;
- if ($total_size > $min_stack) {
- push @stack, "$intro$total_size\n";
- }
- $addr = "0x$1";
- $intro = "$addr $func [$file]:";
- my $padlen = 56 - length($intro);
- while ($padlen > 0) {
- $intro .= ' ';
- $padlen -= 8;
- }
-
- $total_size = 0;
- }
- elsif ($line =~ m/(.*):\s*file format/) {
- $file = $1;
- $file =~ s/\.ko//;
- $lastslash = rindex($file, "/");
- if ($lastslash != -1) {
- $file = substr($file, $lastslash + 1);
- }
- }
- elsif ($line =~ m/$re/) {
- my $size = $1;
- $size = hex($size) if ($size =~ /^0x/);
-
- if ($size > 0xf0000000) {
- $size = - $size;
- $size += 0x80000000;
- $size += 0x80000000;
- }
- next if ($size > 0x10000000);
-
- $total_size += $size;
- }
- elsif (defined $dre && $line =~ m/$dre/) {
- my $size = $1;
-
- $size = hex($size) if ($size =~ /^0x/);
- $total_size += $size;
- }
- elsif (defined $sub) {
- my $size = &$sub($line);
-
- $total_size += $size;
- }
-}
-if ($total_size > $min_stack) {
- push @stack, "$intro$total_size\n";
-}
-
-# Sort output by size (last field) and function name if size is the same
-sub sort_lines {
- my ($a, $b) = @_;
-
- my $num_a = $1 if $a =~ /:\t*(\d+)$/;
- my $num_b = $1 if $b =~ /:\t*(\d+)$/;
- my $func_a = $1 if $a =~ / (.*):/;
- my $func_b = $1 if $b =~ / (.*):/;
-
- if ($num_a != $num_b) {
- return $num_b <=> $num_a;
- } else {
- return $func_a cmp $func_b;
- }
-}
-
-print sort { sort_lines($a, $b) } @stack;
diff --git a/scripts/checkstack.py b/scripts/checkstack.py
new file mode 100755
index 000000000..c3821c860
--- /dev/null
+++ b/scripts/checkstack.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Check the stack usage of functions.
+# Usage: objdump -d vmlinux | checkstack.py [arch] [min_stack]
+
+import platform
+import re
+import sys
+
+X = '[0-9a-f]'
+RE_FUNC = re.compile(rf'^({X}*) <(.*)>:$')
+RE_FILE = re.compile(r'(.*):\s*file format')
+
+
+def arm_push_handler(line):
+ if re.search(r'.*push.*fp, ip, lr, pc}', line):
+ return (line.count(',') + 1) * 4
+ return 0
+
+
+def config_for(arch):
+ """Return (main_re, dynamic_re, sub_fn)."""
+ if re.match(r'^(?:aarch|arm)64$', arch):
+ return (
+ re.compile(rf'^.*stp.*sp, ?#-([0-9]{{1,8}})\]!'),
+ re.compile(rf'^.*sub.*sp, sp, #(0x{X}{{1,8}})'),
+ None)
+ if arch == 'arm':
+ return (re.compile(r'.*sub.*sp, sp, #([0-9]{1,4})'), None,
+ arm_push_handler)
+ if re.match(r'^x86(?:_64)?$', arch) or re.match(r'^i[3456]86$', arch):
+ return (
+ re.compile(rf'^.*[as][du][db] \$(0x{X}{{1,8}}),%(?:e|r)sp$'),
+ re.compile(r'^.*[as][du][db] (%.*),%(?:e|r)sp$'),
+ None)
+ if arch == 'm68k':
+ return (re.compile(r'.*(?:linkw %fp,|addaw )#-([0-9]{1,4})'
+ r'(?:,%sp)?$'), None, None)
+ if arch == 'mips64':
+ return (re.compile(r'.*daddiu.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'mips':
+ return (re.compile(r'.*addiu.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'nios2':
+ return (re.compile(r'.*addi.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'openrisc':
+ return (re.compile(r'.*l\.addi.*r1,r1,-([0-9]{1,8})'), None, None)
+ if arch in ('parisc', 'parisc64'):
+ return (re.compile(rf'.*ldo ({X}{{1,8}})\(sp\),sp'), None, None)
+ if arch == 'powerpc' or re.match(r'^ppc(?:64)?(?:le)?$', arch):
+ return (re.compile(rf'.*st[dw]u.*r1,-({X}{{1,8}})\(r1\)'),
+ None, None)
+ if re.match(r'^s390x?$', arch):
+ return (re.compile(r'.*(?:lay|ag?hi).*\%r15,-([0-9]+)'
+ r'(?:\(\%r15\))?$'), None, None)
+ if arch in ('sparc', 'sparc64'):
+ return (re.compile(r'.*save.*%sp, -([0-9]{1,8}), %sp'), None, None)
+ if re.match(r'^riscv(?:64)?$', arch):
+ return (re.compile(r'.*addi.*sp,sp,-([0-9]{1,8})'), None, None)
+ if re.match(r'^loongarch(?:32|64)?$', arch):
+ return (re.compile(r'.*addi\..*sp, .*sp, -([0-9]{1,8}).*'),
+ None, None)
+ print(f'wrong or unknown architecture "{arch}"')
+ sys.exit(1)
+
+
+def parse_size(s):
+ """
+ Mimics Perl's implicit numeric conversion.
+ In Perl, hex("%eax") silently evaluates to 0.
+ In Python, int("%eax") raises ValueError, so we catch it
+ to maintain 1:1 parity and prevent script crashes.
+ """
+ if not s:
+ return 0
+ try:
+ return int(s, 16) if s.startswith('0x') else int(s)
+ except ValueError:
+ return 0
+
+
+def main(argv):
+ arch = argv[0] if argv else platform.machine()
+ min_stack = int(argv[1]) if len(argv) > 1 and argv[1].isdigit() else 512
+
+ main_re, dyn_re, sub_fn = config_for(arch)
+
+ stack = []
+ func = None
+ file = ''
+ intro = ''
+ total = 0
+
+ for raw in sys.stdin:
+ # Fix: Use rstrip('\r\n') to handle Windows-formatted objdump files safely
+ line = raw.rstrip('\r\n')
+
+ m = RE_FUNC.match(line)
+ if m:
+ func = m.group(2)
+ if total > min_stack:
+ stack.append(f'{intro}{total}\n')
+ addr = m.group(1)
+ intro = f'0x{addr} {func} [{file}]:'
+ pad = 56 - len(intro)
+ while pad > 0:
+ intro += '\t'
+ pad -= 8
+ total = 0
+ continue
+
+ m = RE_FILE.match(line)
+ if m:
+ file = m.group(1).replace('.ko', '')
+ idx = file.rfind('/')
+ if idx != -1:
+ file = file[idx + 1:]
+ continue
+
+ m = main_re.search(line)
+ if m:
+ size = parse_size(m.group(1))
+ if size > 0xf0000000:
+ size = -size + 0x80000000 + 0x80000000
+ if size > 0x10000000:
+ continue
+ total += size
+ continue
+
+ if dyn_re is not None:
+ m = dyn_re.search(line)
+ if m:
+ total += parse_size(m.group(1))
+ continue
+
+ if sub_fn is not None:
+ total += sub_fn(line)
+
+ if total > min_stack:
+ stack.append(f'{intro}{total}\n')
+
+ def key(entry):
+ m = re.search(r':\t*(\d+)$', entry)
+ size = int(m.group(1)) if m else 0
+ m = re.search(r' (.*):', entry)
+ fname = m.group(1) if m else ''
+ return (-size, fname)
+
+ for entry in sorted(stack, key=key):
+ sys.stdout.write(entry)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0