├── .github ├── dependabot.yml └── workflows │ ├── Test.yml │ ├── codeql.yml │ ├── prospector.yml │ └── pyre.yml ├── .gitignore ├── LICENSE.txt ├── README.html ├── README.md ├── TestSrc ├── alloc.h ├── functions.c ├── makefile ├── manual.msu ├── my_lib.c └── no_functions.c ├── WCS.py └── requirements.txt /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/Test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "master" ] 9 | 10 | jobs: 11 | nativeLinux: 12 | name: Native Linux Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.10' 19 | cache: 'pip' # caching pip dependencies 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install -r requirements.txt 24 | - name: Run simple test 25 | run: | 26 | cd TestSrc 27 | make CC=gcc LDFLAGS= 28 | python ../WCS.py 29 | arm: 30 | name: Arm cross compilation Test 31 | runs-on: ubuntu-latest 32 | container: dockcross/linux-armv7 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install -r requirements.txt 39 | - name: Run simple test 40 | run: | 41 | cd TestSrc 42 | make CC=${CC} LDFLAGS= 43 | python ../WCS.py 44 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '38 22 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/prospector.yml: -------------------------------------------------------------------------------- 1 | name: Prospector 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | cache: 'pip' # caching pip dependencies 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | python -m pip install prospector[with_mypy] prospector[with_vulture] mypy vulture 22 | python -m pip install -r requirements.txt 23 | - name: Analysing the code with prospector 24 | run: prospector --with-tool vulture --with-tool mypy --strictness veryhigh --max-line-length 200 *.py 25 | -------------------------------------------------------------------------------- /.github/workflows/pyre.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow integrates Pyre with GitHub's 7 | # Code Scanning feature. 8 | # 9 | # Pyre is a performant type checker for Python compliant with 10 | # PEP 484. Pyre can analyze codebases with millions of lines 11 | # of code incrementally – providing instantaneous feedback 12 | # to developers as they write code. 13 | # 14 | # See https://pyre-check.org 15 | 16 | name: Pyre 17 | 18 | on: 19 | workflow_dispatch: 20 | push: 21 | branches: [ "master" ] 22 | pull_request: 23 | branches: [ "master" ] 24 | 25 | permissions: 26 | contents: read 27 | 28 | jobs: 29 | pyre: 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | submodules: true 39 | 40 | - name: Run Pyre 41 | uses: facebook/pyre-action@12b8d923443ea66cb657facc2e5faac1c8c86e64 42 | with: 43 | # To customize these inputs: 44 | # See https://github.com/facebook/pyre-action#inputs 45 | repo-directory: './' 46 | requirements-path: 'requirements.txt' 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.o 3 | *.c.* 4 | *.su 5 | *.exe 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Peter McKinnis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | README 5 | 6 | 293 | 470 | 471 | 472 |

Worst Case Stack

473 | 474 |

Overview

475 | 476 |

This program is used to do static stack analysis on C source code to determine the maximum stack space used by each function. Source must be compiled using gcc with a number of special flags (see below for details)

477 | 478 |

Useage

479 | 480 |
    481 |
  1. compile *.c sources using gcc and the flags -c, -fdump-rtl-dfinish and -fstack-usage 482 |
  2. 483 |
  3. Run the script python wcs.py
  4. 484 |
485 | 486 |
gcc -c -fdump-rtl-dfinish -fstack-usage main.c my_library.c other.c
487 | wcs.py
488 | 
489 | 490 |

Note: When running wcs.py the current working directory must contain all the relevant input files.

491 | 492 |

Dependencies

493 | 494 |

This script requires python 3. It was written and tested using version 3.4.3

495 | 496 |

Inputs - Files from gcc.

497 | 498 |

The script will search the current directory for sets of files with the names <name>.o, <name>.su and <name>.c.270r.dfinish. If all three are found the script will calculate the worst case stack for every function in the <name>.c.

499 | 500 |

See the usage section for information about how to generate these files.

501 | 502 |

Inputs - Manual Stack Usage Files

503 | 504 |

The scripts also look in files ending with *.msu. These files should contain a whitespace delimited table with function names in the first column, and a decimal integer with the worst case stack usage in the second line

505 | 506 |
my_function 20
507 | do_something 120
508 | __exit 144
509 | 
510 | 511 |

Every line must contain a function / stack pair (empty lines are not permitted). These files can be useful for specifying the worst case stack for functions for which the c-source is not available but the stack usage is known by other means such as inspecting the assembly or run-time testing.

512 | 513 |

Output

514 | 515 |

The script will output a list of functions in a table with the following columns:

516 | 517 |
    518 |
  1. 519 | Translation Unit (e.g. the name of the file where the function is implemented)
  2. 520 |
  3. Function Name
  4. 521 |
  5. 522 | Stack Either the maximum number of bytes during a call to this function (including nested calls at all depths) 523 | or the string unbounded if the maximum cannot be determined because some function in the call tree is recursively defined or makes calls via function pointer.
  6. 524 |
  7. 525 | Unresolved Dependancies A list functions that are called somewhere in the call tree for which there is no 526 | definition in any of the given input files.
  8. 527 |
528 | 529 |

Known Limitations:

530 | 531 |
    532 |
  1. wcs.py can only determine stack usage from *.c source. Calls to compiled libraries (e.g. libc.a) or to assembly functions will result in unbounded (e.g. unknown) stack usage.
  2. 533 |
  3. Functions compiled with the weak attribute will crash this script
  4. 534 |
  5. The actual worst case stack may be greater than reported by this function if outside actors modify the stack. Common offenders are: 535 | 536 |
      537 |
    1. Interrupt handlers
    2. 538 |
    3. Operating system context changes
    4. 539 |
    540 |
  6. 541 |
  7. The use of inline assembly will result in potentially incorrect results. Specifically, if a function uses inline assembly to load or store from the stack, modify the stack pointer, or branch to code that does likewise, expect incorrect results.
    542 |
  8. 543 |
544 | 545 |

The script has no way to detect situations 3 and 4. In the presence of these conditions the script will still complete successfully. Use caution.

546 | 547 |

Notes

548 | 549 |
    550 |
  1. This script assumes little endian byte ordering for object files. If you are compiling for a big-endian system set the byte_order variable to ">" in the file elf.py 551 |
  2. 552 |
553 | 554 |
byte_order = ">" 
555 | 
556 | 557 | 558 | 559 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Worst Case Stack 2 | 3 | ## Overview 4 | This program is used to do static stack analysis on C source code to determine the maximum stack space used by each function. Source must be compiled using gcc with a number of special flags (see below for details) 5 | 6 | ## Useage 7 | 1. compile *.c sources using gcc and the flags `-c`, `-fdump-rtl-dfinish` and `-fstack-usage` 8 | 2. Run the script python wcs.py 9 | 10 | ``` 11 | gcc -c -fdump-rtl-dfinish -fstack-usage main.c my_library.c other.c 12 | wcs.py 13 | ``` 14 | 15 | **Note:** When running `wcs.py` the current working directory must contain all the relevant input files. 16 | 17 | 18 | ## Dependencies 19 | 1. This script requires python 3. It was written and tested using version 3.6.2 20 | 2. Code must be compiled with `gcc`. The script directly calls the utility function `readelf`. `readelf` probably 21 | resides in in the same folder as the gcc executable on your system. 22 | 23 | ## Inputs - Files from gcc. 24 | The script will search the current directory for sets of files with the names `.o`, `.su` and `.c..dfinish`. If all three are found the script will calculate the worst case stack for every function in the translation unit `.c`. The value of `` depends on the version of GCC you use and is auto-detected by the script. In gcc 5.3.1, for example, the value of `` is `270r`. 25 | 26 | See the usage section for information about how to generate these files. 27 | 28 | ## Inputs - Manual Stack Usage Files 29 | The scripts also look in files ending with *.msu. These files should contain a whitespace delimited table with function names in the first column, and a decimal integer with the worst case stack usage in the second line 30 | 31 | ``` 32 | my_function 20 33 | do_something 120 34 | __exit 144 35 | ``` 36 | 37 | Every line must contain a function / stack pair (empty lines are not permitted). These files can be useful for specifying the worst case stack for functions for which the c-source is not available but the stack usage is known by other means such as inspecting the assembly or run-time testing. 38 | 39 | ## Output 40 | The script will output a list of functions in a table with the following columns: 41 | 42 | 1. **Translation Unit** (e.g. the name of the file where the function is implemented) 43 | 2. **Function Name** 44 | 3. **Stack** - The maximum number of bytes used during a call to this function (including nested calls at all depths). 45 | If the maximum cannot be determined because some function in the call tree is recursively 46 | defined or makes calls via function pointer this returns the string `unbounded`. 47 | If there are one or more unresolved dependencies this returns the worst case stack assuming that each unresolved dependency 48 | uses no stack space preceded by the string `unbounded:`. Consider adding a manual stack usage file, for better predictions. 49 | 4. **Unresolved Dependencies** A list functions that are called somewhere in the call tree for which there is no 50 | definition in any of the given input files. 51 | 52 | ## Known Limitations: 53 | 1. wcs.py can only determine stack usage from `*.c` source. Calls to compiled libraries (e.g. libc.a) or to assembly functions will result in `unbounded` (e.g. unknown) stack usage. 54 | 2. The actual worst case stack may be greater than reported by this function if outside actors modify the stack. Common offenders are: 55 | 1. Interrupt handlers 56 | 2. Operating system context changes 57 | 3. The use of inline assembly will result in potentially incorrect results. Specifically, if a function uses inline assembly to load or store from the stack, modify the stack pointer, or branch to code that does likewise, expect incorrect results. 58 | 59 | **The script has no way to detect situations 2 and 3. In the presence of these conditions the script will still complete successfully. Use caution.** 60 | 61 | ## Updates 62 | 63 | ### November 30th, 2017 64 | 1. Removed removed home-brew reading of the symbol table (elf.py) in favor of parsing output from `readelf`. This should improve compatibility. 65 | 2. Fixed 2 spelling errors 66 | 3. Fixed bug when displaying a `multiple declarations` error 67 | 68 | ### April 25, 2018 69 | 1. Added autodetection of the RTL extension (e.g. '270r') 70 | 2. Added better error message 71 | -------------------------------------------------------------------------------- /TestSrc/alloc.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #define STACK_ALLOC(x) { \ 4 | volatile char mem[x] = {}; \ 5 | mem[0] = mem[x-1]; \ 6 | } 7 | -------------------------------------------------------------------------------- /TestSrc/functions.c: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "alloc.h" 4 | 5 | static int static1(char* str, int len); 6 | int static2(float a, float b); 7 | void static3(int a); 8 | void static4(int a); 9 | int static5(int a); 10 | void recursive1(void); 11 | void recursive2a(void); 12 | void recursive2b(void); 13 | void recursive2c(void); 14 | void recursive2d(void); 15 | void indirect1(int i); 16 | void indirect2(void); 17 | void indirect3(int i, int offset); 18 | void indirect4(int i); 19 | void inline_asm1(int i); 20 | 21 | void extern_prv_fxn1(void); 22 | typedef void(*action)(void); 23 | typedef void(*action_int)(int); 24 | 25 | 26 | 27 | static int static1(char* str, int len) { 28 | STACK_ALLOC(240); 29 | return len; 30 | } 31 | 32 | int static2(float a, float b) { 33 | STACK_ALLOC(140); 34 | return (int)b; 35 | } 36 | 37 | void static3(int a) { 38 | static1("Hi", 2); 39 | static2(1.5,3.5); 40 | } 41 | 42 | void static4(int a) { 43 | static1("Hi", 2); 44 | static2(1.5, 3.5); 45 | static3(5); 46 | } 47 | 48 | int static5(int a) { 49 | extern_prv_fxn1(); 50 | extern_prv_fxn1(); 51 | } 52 | 53 | 54 | void recursive1(void) { 55 | STACK_ALLOC(40); 56 | recursive1(); 57 | } 58 | 59 | void recursive2a(void) { 60 | STACK_ALLOC(8); 61 | recursive2b(); 62 | } 63 | 64 | void recursive2b(void) { 65 | STACK_ALLOC(144); 66 | recursive2c(); 67 | } 68 | 69 | void recursive2c(void) { 70 | STACK_ALLOC(990); 71 | recursive2d(); 72 | } 73 | 74 | void recursive2d(void) { 75 | STACK_ALLOC(120); 76 | recursive2a(); 77 | } 78 | 79 | 80 | typedef void(*action)(void); 81 | typedef void(*action_int)(int); 82 | 83 | 84 | void indirect1(int i) { 85 | 86 | // Use of function pointers 87 | action_int a; 88 | 89 | if (i) { 90 | a = static3; 91 | } 92 | else { 93 | a = static4; 94 | } 95 | 96 | a(i); 97 | 98 | } 99 | 100 | void indirect2(void) { 101 | 102 | // Call A function at address 0x1205 103 | int x = 0x1205; 104 | action a = (action*)x; 105 | a(); 106 | 107 | } 108 | 109 | void indirect3(int i, int offset) { 110 | 111 | // Use of function pointer math (un 112 | action_int a; 113 | 114 | if (a) { 115 | a = &static3; 116 | } 117 | else { 118 | a = &static4; 119 | } 120 | 121 | a = (action_int)(((char *)a) + offset); 122 | 123 | a(i); 124 | 125 | } 126 | 127 | void indirect4(int i) { 128 | 129 | // Use of function pointer math (un 130 | action_int a = static3; 131 | a(i); 132 | 133 | } 134 | 135 | void function_with_really_long_name_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA(int i) { 136 | 137 | } 138 | 139 | // There are multiple definitions of this in multiple files 140 | void static prv_fxn1(void){ 141 | STACK_ALLOC(100); 142 | } 143 | 144 | void functions_prv_fxn1(void) { 145 | prv_fxn1(); 146 | } 147 | 148 | int main(void) { 149 | static1("Hello", 5); 150 | static2(11.3, 12.3); 151 | static3(5); 152 | static4(6); 153 | static5(7); 154 | recursive1(); 155 | recursive2a(); 156 | recursive2b(); 157 | recursive2c(); 158 | recursive2d(); 159 | indirect1(5); 160 | indirect1(0); 161 | indirect2(); 162 | indirect3(8,9); 163 | 164 | functions_prv_fxn1(); // This should use about 100 bytes of the stack 165 | extern_prv_fxn1(); // This should use about 1000 bytes of the stack 166 | return 0; 167 | } 168 | 169 | int main3(void) { 170 | main(); 171 | } 172 | 173 | -------------------------------------------------------------------------------- /TestSrc/makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | CC=arm-none-eabi-gcc 4 | CFLAGS=-c -gdwarf-3 -fstack-usage -fdump-rtl-dfinish 5 | LDFLAGS= -Wall --specs=nosys.specs 6 | SOURCES=functions.c my_lib.c no_functions.c 7 | OBJECTS=$(SOURCES:.c=.o) 8 | EXECUTABLE=test.exe 9 | 10 | all: $(SOURCES) $(EXECUTABLE) 11 | 12 | $(EXECUTABLE): $(OBJECTS) 13 | $(CC) $(LDFLAGS) $(OBJECTS) -o $@ 14 | 15 | .c.o: 16 | $(CC) $(CFLAGS) $< -o $@ 17 | 18 | clean: 19 | rm *.o *.c.* *.su -------------------------------------------------------------------------------- /TestSrc/manual.msu: -------------------------------------------------------------------------------- 1 | memset 8 -------------------------------------------------------------------------------- /TestSrc/my_lib.c: -------------------------------------------------------------------------------- 1 | 2 | #include "alloc.h" 3 | 4 | void extern_prv_fxn1(void); 5 | void static prv_fxn1(void); 6 | 7 | void extern_prv_fxn1(void) { 8 | prv_fxn1(); 9 | } 10 | 11 | // There are multiple definitions of this in multiple files 12 | void static prv_fxn1(void){ 13 | STACK_ALLOC(1000); 14 | } -------------------------------------------------------------------------------- /TestSrc/no_functions.c: -------------------------------------------------------------------------------- 1 | 2 | 3 | int var_1 = 10; 4 | int var_2 = 20; 5 | -------------------------------------------------------------------------------- /WCS.py: -------------------------------------------------------------------------------- 1 | # pylint: disable = invalid-name, too-few-public-methods 2 | 3 | import re 4 | import pprint 5 | import os 6 | import sys 7 | from typing import List, Tuple, Dict, Any 8 | from subprocess import check_output 9 | 10 | # Constants 11 | rtl_ext_end = ".dfinish" 12 | su_ext = '.su' 13 | obj_ext = '.o' 14 | manual_ext = '.msu' 15 | read_elf_path = os.getenv("CROSS_COMPILE", "") + "readelf" 16 | stdout_encoding = "utf-8" # System dependant 17 | 18 | 19 | class Printable: 20 | def __repr__(self) -> str: 21 | return "<" + type(self).__name__ + "> " + pprint.pformat(vars(self), indent=4, width=1) 22 | 23 | 24 | class Symbol(Printable): 25 | # value: int = -1 26 | # size: int = -1 27 | type: str = "uninitialized" 28 | binding: str = "uninitialized" 29 | name: str = "uninitialized" 30 | 31 | 32 | CallNode = Dict[str, Any] 33 | 34 | 35 | def calc_wcs(fxn_dict2: CallNode, parents: List[CallNode]) -> None: 36 | """ 37 | Calculates the worst case stack for a fxn that is declared (or called from) in a given file. 38 | :param parents: This function gets called recursively through the call graph. If a function has recursion the 39 | tuple file, fxn will be in the parents stack and everything between the top of the stack and the matching entry 40 | has recursion. 41 | :return: 42 | """ 43 | 44 | # If the wcs is already known, then nothing to do 45 | if 'wcs' in fxn_dict2: 46 | return 47 | 48 | # Check for pointer calls 49 | if fxn_dict2['has_ptr_call']: 50 | fxn_dict2['wcs'] = 'unbounded' 51 | return 52 | 53 | # Check for recursion 54 | if fxn_dict2 in parents: 55 | fxn_dict2['wcs'] = 'unbounded' 56 | return 57 | 58 | # Calculate WCS 59 | call_max = 0 60 | for call_dict in fxn_dict2['r_calls']: 61 | 62 | # Calculate the WCS for the called function 63 | calc_wcs(call_dict, parents + [fxn_dict2]) 64 | 65 | # If the called function is unbounded, so is this function 66 | if call_dict['wcs'] == 'unbounded': 67 | fxn_dict2['wcs'] = 'unbounded' 68 | return 69 | 70 | # Keep track of the call with the largest stack use 71 | call_max = max(call_max, call_dict['wcs']) 72 | 73 | # Propagate Unresolved Calls 74 | for unresolved_call in call_dict['unresolved_calls']: 75 | fxn_dict2['unresolved_calls'].add(unresolved_call) 76 | 77 | fxn_dict2['wcs'] = call_max + fxn_dict2['local_stack'] 78 | 79 | 80 | class CallGraph: 81 | globals: Dict[str, CallNode] = {} 82 | locals: Dict[str, Dict[str, CallNode]] = {} 83 | weak: Dict[str, CallNode] = {} 84 | 85 | def read_obj(self, tu: str) -> None: 86 | """ 87 | Reads the file tu.o and gets the binding (global or local) for each function 88 | :param self: a object used to store information about each function, results go here 89 | :param tu: name of the translation unit (e.g. for main.c, this would be 'main') 90 | """ 91 | 92 | for s in read_symbols(tu[0:tu.rindex(".")] + obj_ext): 93 | 94 | if s.type == 'FUNC': 95 | if s.binding == 'GLOBAL': 96 | # Check for multiple declarations 97 | if s.name in self.globals or s.name in self.locals: 98 | raise Exception(f'Multiple declarations of {s.name}') 99 | self.globals[s.name] = {'tu': tu, 'name': s.name, 'binding': s.binding} 100 | elif s.binding == 'LOCAL': 101 | # Check for multiple declarations 102 | if s.name in self.locals and tu in self.locals[s.name]: 103 | raise Exception(f'Multiple declarations of {s.name}') 104 | 105 | if s.name not in self.locals: 106 | self.locals[s.name] = {} 107 | 108 | self.locals[s.name][tu] = {'tu': tu, 'name': s.name, 'binding': s.binding} 109 | elif s.binding == 'WEAK': 110 | if s.name in self.weak: 111 | raise Exception(f'Multiple declarations of {s.name}') 112 | self.weak[s.name] = {'tu': tu, 'name': s.name, 'binding': s.binding} 113 | else: 114 | raise Exception(f'Error Unknown Binding "{s.binding}" for symbol: {s.name}') 115 | 116 | def find_fxn(self, tu: str, fxn: str): 117 | """ 118 | Looks up the dictionary associated with the function. 119 | :param self: a object used to store information about each function 120 | :param tu: The translation unit in which to look for locals functions 121 | :param fxn: The function name 122 | :return: the dictionary for the given function or None 123 | """ 124 | 125 | if fxn in self.globals: 126 | return self.globals[fxn] 127 | try: 128 | return self.locals[fxn][tu] 129 | except KeyError: 130 | return None 131 | 132 | def find_demangled_fxn(self, tu: str, fxn: str): 133 | """ 134 | Looks up the dictionary associated with the function. 135 | :param self: a object used to store information about each function 136 | :param tu: The translation unit in which to look for locals functions 137 | :param fxn: The function name 138 | :return: the dictionary for the given function or None 139 | """ 140 | for f in self.globals.values(): 141 | if 'demangledName' in f: 142 | if f['demangledName'] == fxn: 143 | return f 144 | for f in self.locals.values(): 145 | if tu in f: 146 | if 'demangledName' in f[tu]: 147 | if f[tu]['demangledName'] == fxn: 148 | return f[tu] 149 | return None 150 | 151 | def read_rtl(self, tu: str, rtl_ext: str) -> None: 152 | """ 153 | Read an RTL file and finds callees for each function and if there are calls via function pointer. 154 | :param self: a object used to store information about each function, results go here 155 | :param tu: the translation unit 156 | """ 157 | 158 | # Construct A Call Graph 159 | function = re.compile(r'^;; Function (.*) \((\S+), funcdef_no=\d+(, [a-z_]+=\d+)*\)( \([a-z ]+\))?$') 160 | static_call = re.compile(r'^.*\(call.*"(.*)".*$') 161 | other_call = re.compile(r'^.*call .*$') 162 | 163 | with open(tu + rtl_ext, "rt", encoding="latin_1") as file_: 164 | for line_ in file_: 165 | m = function.match(line_) 166 | if m: 167 | fxn_name = m.group(2) 168 | fxn_dict2 = self.find_fxn(tu, fxn_name) 169 | if not fxn_dict2: 170 | pprint.pprint(self) 171 | raise Exception(f"Error locating function {fxn_name} in {tu}") 172 | 173 | fxn_dict2['demangledName'] = m.group(1) 174 | fxn_dict2['calls'] = set() 175 | fxn_dict2['has_ptr_call'] = False 176 | continue 177 | 178 | m = static_call.match(line_) 179 | if m: 180 | fxn_dict2['calls'].add(m.group(1)) 181 | # print("Call: {0} -> {1}".format(current_fxn, m.group(1))) 182 | continue 183 | 184 | m = other_call.match(line_) 185 | if m: 186 | fxn_dict2['has_ptr_call'] = True 187 | continue 188 | 189 | def read_su(self, tu: str) -> None: 190 | """ 191 | Reads the 'local_stack' for each function. Local stack ignores stack used by callees. 192 | :param self: a object used to store information about each function, results go here 193 | :param tu: the translation unit 194 | :return: 195 | """ 196 | # Needs to be able to handle both cases, i.e.: 197 | # c:\\userlibs\\gcc\\arm-none-eabi\\include\\assert.h:41:6:__assert_func 16 static 198 | # main.c:113:6:vAssertCalled 8 static 199 | # Now Matches six groups https://regex101.com/r/Imi0sq/1 200 | 201 | su_line = re.compile(r'^(.+):(\d+):(\d+):(.+)\t(\d+)\t(\S+)$') 202 | i = 1 203 | 204 | with open(tu[0:tu.rindex(".")] + su_ext, "rt", encoding="latin_1") as file_: 205 | for line in file_: 206 | m = su_line.match(line) 207 | if m: 208 | fxn = m.group(4) 209 | fxn_dict2 = self.find_demangled_fxn(tu, fxn) 210 | fxn_dict2['local_stack'] = int(m.group(5)) 211 | else: 212 | print(f"error parsing line {i} in file {tu}") 213 | i += 1 214 | 215 | def read_manual(self, file: str) -> None: 216 | """ 217 | reads the manual stack useage files. 218 | :param self: a object used to store information about each function, results go here 219 | :param file: the file name 220 | """ 221 | 222 | with open(file, "rt", encoding="latin_1") as file_: 223 | for line in file_: 224 | fxn, stack_sz = line.split() 225 | if fxn in self.globals: 226 | raise Exception(f"Redeclared Function {fxn}") 227 | self.globals[fxn] = {'wcs': int(stack_sz), 228 | 'calls': set(), 229 | 'has_ptr_call': False, 230 | 'local_stack': int(stack_sz), 231 | 'is_manual': True, 232 | 'name': fxn, 233 | 'demangledName': fxn, 234 | 'tu': '#MANUAL', 235 | 'binding': 'GLOBAL'} 236 | 237 | def validate_all_data(self) -> None: 238 | """ 239 | Check that every entry in the call graph has the following fields: 240 | .calls, .has_ptr_call, .local_stack, .scope, .src_line 241 | """ 242 | 243 | def validate_dict(d): 244 | if not ('calls' in d and 'has_ptr_call' in d and 'local_stack' in d 245 | and 'name' in d and 'tu' in d): 246 | print(f"Error data is missing in fxn dictionary {d}") 247 | 248 | # Loop through every global and local function 249 | # and resolve each call, save results in r_calls 250 | for fxn_dict2 in self.globals.values(): 251 | validate_dict(fxn_dict2) 252 | 253 | for l_dict in self.locals.values(): 254 | for fxn_dict2 in l_dict.values(): 255 | validate_dict(fxn_dict2) 256 | 257 | def resolve_all_calls(self) -> None: 258 | def resolve_calls(fxn_dict2: CallNode) -> None: 259 | fxn_dict2['r_calls'] = [] 260 | fxn_dict2['unresolved_calls'] = set() 261 | 262 | for call in fxn_dict2['calls']: 263 | call_dict = self.find_fxn(fxn_dict2['tu'], call) 264 | if call_dict: 265 | fxn_dict2['r_calls'].append(call_dict) 266 | else: 267 | fxn_dict2['unresolved_calls'].add(call) 268 | 269 | # Loop through every global and local function 270 | # and resolve each call, save results in r_calls 271 | for fxn_dict in self.globals.values(): 272 | resolve_calls(fxn_dict) 273 | 274 | for l_dict in self.locals.values(): 275 | for fxn_dict in l_dict.values(): 276 | resolve_calls(fxn_dict) 277 | 278 | def calc_all_wcs(self) -> None: 279 | # Loop through every global and local function 280 | # and resolve each call, save results in r_calls 281 | for fxn_dict in self.globals.values(): 282 | calc_wcs(fxn_dict, []) 283 | 284 | for l_dict in self.locals.values(): 285 | for fxn_dict in l_dict.values(): 286 | calc_wcs(fxn_dict, []) 287 | 288 | def print_all_fxns(self) -> None: 289 | 290 | def print_fxn(row_format: str, fxn_dict2: CallNode) -> None: 291 | unresolved = fxn_dict2['unresolved_calls'] 292 | stack = str(fxn_dict2['wcs']) 293 | if unresolved: 294 | unresolved_str = f"({' ,'.join(unresolved)})" 295 | if stack != 'unbounded': 296 | stack = "unbounded:" + stack 297 | else: 298 | unresolved_str = '' 299 | 300 | print(row_format.format(fxn_dict2['tu'], fxn_dict2['demangledName'], stack, unresolved_str)) 301 | 302 | def get_order(val) -> int: 303 | return 1 if val == 'unbounded' else -val 304 | 305 | # Loop through every global and local function 306 | # and resolve each call, save results in r_calls 307 | d_list = [] 308 | for fxn_dict in self.globals.values(): 309 | d_list.append(fxn_dict) 310 | 311 | for l_dict in self.locals.values(): 312 | for fxn_dict in l_dict.values(): 313 | d_list.append(fxn_dict) 314 | 315 | d_list.sort(key=lambda item: get_order(item['wcs'])) 316 | 317 | # Calculate table width 318 | tu_width = max(max(len(d['tu']) for d in d_list), 16) 319 | name_width = max(max(len(d['name']) for d in d_list), 13) 320 | row_format = "{:<" + str(tu_width + 2) + "} {:<" + str(name_width + 2) + "} {:>14} {:<17}" 321 | 322 | # Print out the table 323 | print("") 324 | print(row_format.format('Translation Unit', 'Function Name', 'Stack', 'Unresolved Dependencies')) 325 | for d in d_list: 326 | print_fxn(row_format, d) 327 | 328 | 329 | def read_symbols(file: str) -> List[Symbol]: 330 | 331 | def to_symbol(read_elf_line: str) -> Symbol: 332 | v = read_elf_line.split() 333 | 334 | s2 = Symbol() 335 | # s2.value = int(v[1], 16) 336 | # if ('x' in v[2]): 337 | # #raise Exception(f'Mixed symbol sizes in \'{v}\' ') 338 | # s2.size=int(v[2].split('x')[1],16) 339 | # else: 340 | # s2.size = int(v[2]) 341 | s2.type = v[3] 342 | s2.binding = v[4] 343 | s2.name = v[7] if len(v) >= 8 else "" 344 | 345 | return s2 346 | output = check_output([read_elf_path, "-s", "-W", file]).decode(stdout_encoding) 347 | lines = output.splitlines()[3:] 348 | return [to_symbol(line) for line in lines] 349 | 350 | 351 | def find_rtl_ext() -> str: 352 | # Find the rtl_extension 353 | 354 | for _, _, filenames in os.walk('.'): 355 | for f in filenames: 356 | if f.endswith(rtl_ext_end): 357 | rtl_ext = f[f[:-len(rtl_ext_end)].rindex("."):] 358 | print("rtl_ext = " + rtl_ext) 359 | return rtl_ext 360 | 361 | print("Could not find any files ending with '.dfinish'. Check that the script is being run from the correct " 362 | "directory. Check that the code was compiled with the correct flags") 363 | sys.exit(-1) 364 | 365 | 366 | def find_files(rtl_ext: str) -> Tuple[List[str], List[str]]: 367 | tu: List[str] = [] 368 | manual: List[str] = [] 369 | all_files: List[str] = [] 370 | for root, _, filenames in os.walk('.'): 371 | for filename in filenames: 372 | all_files.append(os.path.join(root, filename)) 373 | for f in [f for f in all_files if os.path.isfile(f) and f.endswith(rtl_ext)]: 374 | base = f[0:-len(rtl_ext)] 375 | short_base = base[0:base.rindex(".")] 376 | if short_base + su_ext in all_files and short_base + obj_ext in all_files: 377 | tu.append(base) 378 | print(f'Reading: {base}{rtl_ext}, {short_base}{su_ext}, {short_base}{obj_ext}') 379 | 380 | for f in [f for f in all_files if os.path.isfile(f) and f.endswith(manual_ext)]: 381 | manual.append(f) 382 | print(f'Reading: {f}') 383 | 384 | # Print some diagnostic messages 385 | if not tu: 386 | print("Could not find any translation units to analyse") 387 | sys.exit(-1) 388 | 389 | return tu, manual 390 | 391 | 392 | def main() -> None: 393 | # Find the appropriate RTL extension 394 | rtl_ext = find_rtl_ext() 395 | 396 | # Find all input files 397 | call_graph: CallGraph = CallGraph() 398 | tu_list, manual_list = find_files(rtl_ext) 399 | 400 | # Read the input files 401 | for tu in tu_list: 402 | call_graph.read_obj(tu) # This must be first 403 | 404 | for fxn in call_graph.weak.values(): 405 | if fxn['name'] not in call_graph.globals: 406 | call_graph.globals[fxn['name']] = fxn 407 | 408 | for tu in tu_list: 409 | call_graph.read_rtl(tu, rtl_ext) 410 | for tu in tu_list: 411 | call_graph.read_su(tu) 412 | 413 | # Read manual files 414 | for m in manual_list: 415 | call_graph.read_manual(m) 416 | 417 | # Validate Data 418 | call_graph.validate_all_data() 419 | 420 | # Resolve All Function Calls 421 | call_graph.resolve_all_calls() 422 | 423 | # Calculate Worst Case Stack For Each Function 424 | call_graph.calc_all_wcs() 425 | 426 | # Print A Nice Message With Each Function and the WCS 427 | call_graph.print_all_fxns() 428 | 429 | 430 | main() 431 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------