pyocd / pyOCD

Open source Python library for programming and debugging Arm Cortex-M microcontrollers
https://pyocd.io
Apache License 2.0
1.12k stars 481 forks source link

gdb server doesn't provide information about memory regions with device registers #960

Closed vznncv closed 1 year ago

vznncv commented 4 years ago

Description of defect

gdbserver of pyocd doesn't return memory regions of peripheral registers with $qXfer:memory-map:read command. Only flash and SRAM regions are returned.

It doesn't prevent debugging, but causes that gdb cannot read peripheral registers that are defined by SVD files.

Probably pyocd should return memory map that contains device register regions, as some software (gdb with QtCreator) uses this information to display peripheral registers.

QtCreator example

  1. debugging with PyOCD:

    pyocd_original

    The info mem gdb command shows the following results:

    • server command:
      $ pyocd gdbserver --target stm32f303vctx
    • client command:

      $ arm-none-eabi-gdb
      ...
      (gdb) target remote localhost:3333
      Remote debugging using localhost:3333
      ...
      
      (gdb) info mem
      Using memory regions provided by the target.
      Num Enb Low Addr   High Addr  Attrs 
      0   y      0x08000000 0x08040000 flash blocksize 0x800 nocache 
      1   y      0x10000000 0x10002000 rw nocache 
      2   y      0x20000000 0x2000c000 rw nocache

      note: 0x48001000 address absents in the memory regions

  2. debugging with OpenOCD:

    openocd

    The info mem gdb command shows the following results:

    • server command:
      $ openocd -f 'board/stm32f3discovery.cfg' -f 'interface/stlink-v2-1.cfg'
    • client command:

      $ arm-none-eabi-gdb
      ...
      (gdb) target remote localhost:3333
      Remote debugging using localhost:3333
      ...
      
      (gdb) info mem
      Using memory regions provided by the target.
      Num Enb Low Addr   High Addr  Attrs 
      0   y      0x00000000 0x08000000 rw nocache 
      1   y      0x08000000 0x08040000 flash blocksize 0x800 nocache 
      2   y      0x08040000 0x100000000 rw nocache 

      note: the region addresses aren't correct (some addresses are invalid), but 0x48001000 address presents in the last region, so QtCreator is able to display peripheral registers

  3. If I add device regions with monkey patching using the following script, it will also work:

    pyocd_patched

    #!/usr/bin/env python3
    import sys
    from unittest.mock import patch
    
    from pyocd.__main__ import main as pyocd_main
    from pyocd.core.memory_map import DeviceRegion
    from pyocd.gdbserver.context_facade import GDBDebugContextFacade
    
    def main(args=None):
        # add "gdbserver" command
        sys.argv.insert(1, 'gdbserver')
    
        original_get_memory_map_xml = GDBDebugContextFacade.get_memory_map_xml
    
        def patched_get_memory_map_xml(self):
            memory_map = self._context.core.memory_map
    
            # save original regions
            original_regions = memory_map.regions.copy()
    
            # modify regions
            # see: "2.2.3 Behavior of memory accesses"
            # in the https://www.st.com/resource/en/programming_manual/dm00046982-stm32-cortex-m4-mcus-and-mpus-programming-manual-stmicroelectronics.pdf
            memory_map.regions.extend([
                DeviceRegion(start=0x40000000, end=0x5FFFFFFF),  # STM32 Cortex M4 "Peripheral"
                DeviceRegion(start=0xED000000, end=0xED0FFFFF),  # STM32 Cortex M4 "Private peripheral bus"
                DeviceRegion(start=0xED100000, end=0xFFFFFFFF),  # STM32 Cortex M4 "Memory mapped peripherals"
            ])
    
            # build memory map
            memory_map_xml = original_get_memory_map_xml(self)
    
            # restore original regions
            memory_map.regions[:] = original_regions
    
            return memory_map_xml
    
        # patch get_memory_map_xml to add device regions and run pyocd gdbserver
        with patch.object(GDBDebugContextFacade, 'get_memory_map_xml', patched_get_memory_map_xml):
            pyocd_main()
    
    if __name__ == '__main__':
        main()

    The info mem gdb command shows the following results:

    • server command:
      $ pyocd  ./pyocd_openocd_server.py --target stm32f303vctx
    • client command:

      $ arm-none-eabi-gdb
      ...
      (gdb) target remote localhost:3333
      Remote debugging using localhost:3333
      ...
      
      (gdb) info mem
      Using memory regions provided by the target.
      Num Enb Low Addr   High Addr  Attrs 
      0   y      0x08000000 0x08040000 flash blocksize 0x800 nocache 
      1   y      0x10000000 0x10002000 rw nocache 
      2   y      0x20000000 0x2000c000 rw nocache 
      3   y      0x40000000 0x60000000 rw nocache 
      4   y      0xed000000 0xed100000 rw nocache 
      5   y      0xed100000 0x100000000 rw nocache

      note: the 0x48001000 address presents in a memory region, so QtCreator is able to read it

Toolchain(s) (name and version) and target

Possible solution

Add common "Peripheral" regions for all targets with corresponding cortex-m cores according cortex-m documentation:

https://developer.arm.com/documentation/ddi0439/b/Programmers-Model/System-address-map

flit commented 4 years ago

Thanks a lot for the really detailed issue! I agree that it would be nice to have the device regions represented correctly in the memory map.

The canonical solution for this is to issue the command set mem inaccessible-by-default off to gdb. This enables access to the full 4GB address space, so the memory map is only used to determine read/write-ability and set the gdb cache policy.

I've also thought about always adding in the standard Cortex-M Peripheral range. The problem with that is it adds a very large (256 MB) range that is normally very sparsely populated with peripherals. In addition, a number of devices have peripherals in different regions, and it wouldn't cover the PPB and Vendor PPB regions. Those could of course be added, but you end up with something like 1/3 the address space covered by a Device region. 😄

Probably the best option is to examine the SVD file and extract a region that covers all defined peripherals. Or several regions, with a heuristic for when to split based on how far apart nearly-contiguous regions are. The issue here is that parsing SVD files is currently quite slow, which would negatively affect pyOCD's startup performance with a very noticeably delay. We could compromise by making it a user-configurable option that is disabled by default.

One issue with all options is they don't distinguish between S and NS attributions in v8-M devices with TrustZone-M. Not sure how to address this without proper information in the CMSIS-Pack. (This isn't covered by the SVD data.)

flit commented 4 years ago

Another possibility that's already supported is to add memory regions with a user script.

It would be possible to have a session option to add memory regions, but doing it via the user script is probably better anyway.

vznncv commented 4 years ago

Thanks for advice about user script. This approach with adding extra memory regions inside user script suites me.

flit commented 4 years ago

Glad the user script will work for now. I'm going to continue working towards a good solution based on SVD and other CMSIS-Pack data. Thanks for raising the problem.

yinfangchen commented 4 years ago

@flit @vznncv Thanks for the info provided. I also meet this issue in when using arm gdb, so it is very useful for me.

Besides, I wanna ask whether it is possible (or there is an easy way) to read all of the I/O traffic inside the memory regions of peripheral registers when the MCU is running by leveraging pyOCD? What I understand from above is that we could “see” this peripheral memory region when debugging the firmware, which means we should stop the firmware/binary and run the gdb command e.g. x/10x [somewhere between 0x40000000 0x60000000]. However, what I want to achieve first in my research project is to export all of the changes inside peripheral memory regions by sequence when the firmware is continuously running. You could regard this as you not only run continue command inside GDB, but also output the peripheral region at the same time. Is it possible to have this sort of automation by using pyOCD (sorry I am a newbie for pyOCD😅..)?

Thanks a lot if you could provide any ideas/suggestions!

flit commented 4 years ago

Hi @DarknessChen

If I understand correctly, it sounds like you want to record every read or write to the peripheral memory region. I'm afraid it isn't directly possible on any off the shelf system. It would require a system-level bus trace capability that would record every bus transaction (read or write) to the peripheral memory region (or whatever address range you want).

There are some options, however.

  1. Instrument your code. Modify the peripheral register access macros to record the operation somewhere like a ring buffer.
  2. Similarly, you could also redirect all peripheral accesses through a couple functions, then set breakpoints that log a message in the debugger (called logpoints in some debuggers). This would be very slow.
  3. Use instruction + data trace to reconstruct peripheral accesses. Only possible on Cortex-A/R devices, because Cortex-M cores do not support data trace (only instruction trace).
  4. Use a system simulator. If you are doing your research in a university, you can contact the Arm University Program (google it) to get access to tools like the different Arm models and simulators.
  5. Use a microarchitectural simulator like gem5.
  6. Build your own system in RTL and run in an RTL simulator, then examine the recorded signals in a wave viewer.
  7. Build your system with a custom trace feature on the bus fabric and run on an FPGA. For these RTL options, the Arm University Program can also provide materials to help you.

Hope that helps!

yinfangchen commented 4 years ago

Yeah, what you understand is exactly what I want to do. These advice is really helpful for me.

To be more specific, currently I want to directly record every read or write to the peripheral memory region when the real-world IoT device is running (a lot of vendors may only provide the executable so we should assume we only have the binary file without the source code). I am exploring that whether I could get those I/O traffic on-the-fly via Jtag, but as you said it may be not possible directly.

What's more, I guess another way is to run the firmware in QEMU (an emulator) and forward the I/O operations to the real hardware, which may give me a chance to observe the read/write operation. Actually this is what avatar2 could achieve, but it is another business..

Anyway, I am very glad to hear the ideas from you, if you have any other suggestions please let me know.

Thank you so much @flit !

flit commented 4 years ago

That's right, another emulator such as QEMU is a good option. And even if it doesn't already give you the observation ability you need, you have the source code so you can add it. 😄

Are you doing this for security research? I'd like to hear a little more about the purpose of your research if you don't mind. If you want to talk more privately, just send me an email (see my github profile for the address).

yinfangchen commented 4 years ago

Yeah, you are right. Open source tool deserves our appreciation. And it is amazing that QEMU was firstly made by a single person..