This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

Type promotion bug?

Specific pointer -> generic pointer -> boolean (i.e. implicit compare to 0) seems not to work as expected.

void main()
{
  char* p;
  char* q;
  char xdata* xp = NULL;

  bit fail = FALSE;

  // This fails.
  if ( p = xp )
  {
    fail = TRUE;
  }

  // This is ok.
  if ( ( q = xp ) == NULL )
  {
    fail = TRUE;
  }
}

Parents
  • It's only Keil C, and particular it's memory-specific pointers, that make it hard.

    NULL is not difficult. It is the 8051 architecture that throws a wrench into things.

    The 8051 is not a vonNeumann architecture and it is not linear. In the 8051, you have:

    • 256 bytes of IDATA (which starts at address 0x00 and which is addressed using a single-byte (8-bit) pointer. In this memory a NULL pointer has the value 0x00. IDATA is accessed using an idata pointer (int idata *).

    • 64K bytes of XDATA (which starts at address 0x0000 and which is addressed using a two-byte (16-bit) pointer. In this memory model a NULL pointer has the value 0x0000. XDATA is accessed using an xdata pointer (int xdata *).

    • 64K bytes of CODE (which starts at address 0x0000 and which is addressed using a two-byte (16-bit) pointer. In this memory model a NULL pointer has the value 0x0000. CODE is accessed using a code pointer (int code *).

    If you only deal with typed pointers or memory-specific pointers (idata *, xdata *, code *, ...) NULL works perfectly. But, if you only have memory-specific pointers (and no generic pointers) you must have multiple copies of library functions that accept pointers. For example, the (generic pointer) strcpy function is very well known:

    strcpy (char *d, char *s);

    If we supported only memory-specific pointers, we would require at least six different strcpy routines:

    strcpy_ii (char idata *d, char idata *s);
    strcpy_ix (char idata *d, char xdata *s);
    strcpy_ic (char idata *d, char code *s);
    
    strcpy_xi (char xdata *d, char idata *s);
    strcpy_xx (char xdata *d, char xdata *s);
    strcpy_xc (char xdata *d, char code *s);

    That would make the library HUGE and also make it really inconvenient to write embedded programs. But, NULL would always work just GREAT!

    Generic pointers are used to solve several problems--primarily to make the address space linear. Basically, the idea is that each memory area needs a memory selector (or segment) byte. So, IDATA is 0x00, XDATA is 0x01, and CODE is 0xFF.

    So...

    • A generic pointer to code address 0x0000 is 0xFF0000.

    • A generic pointer to xdata address 0x0000 is 0x010000.

    • A generic pointer to idata address 0x0000 is 0x000000.

    Now the question is which one of these is a pointer to offset 0 (in a particular memory space) and which is NULL? Based on the definition of NULL (which is defined as ((void *) 0) ) the only candidate for NULL is a generic pointer to idata address 0x00.

    The correct change would be to not handle (pointer == 0) or (pointer == NULL) by casting the pointer to some other type, but rather by casting the zero to the given pointer's type.

    No change is required. The Keil compiler already does this. The following program illustrates that.

    #include <REG52.H>
    #include <stdio.h>
    
    void main (void)
    {
    char idata *ip = NULL;
    char xdata *xp = NULL;
    char code *cp = NULL;
    char *gp = NULL;
    
    SCON  = 0x50;		        /* SCON: mode 1, 8-bit UART, enable rcvr      */
    TMOD |= 0x20;               /* TMOD: timer 1, mode 2, 8-bit reload        */
    TH1   = 221;                /* TH1:  reload value for 1200 baud @ 16MHz   */
    TR1   = 1;                  /* TR1:  timer 1 run                          */
    TI    = 1;                  /* TI:   set TI to send first char of UART    */
    
    if (ip == NULL) printf ("IP is NULL\n");
    if (xp == NULL) printf ("XP is NULL\n");
    if (cp == NULL) printf ("CP is NULL\n");
    if (gp == NULL) printf ("GP is NULL\n");
    
    if ((gp = ip) == NULL) printf ("(GP = IP) is NULL\n");
    if ((gp = xp) == NULL) printf ("(GP = XP) is NULL\n");
    if ((gp = cp) == NULL) printf ("(GP = CP) is NULL\n");
    
    while (1);
    }

    The original question refered to the following code (to which I added #defines):

    #define TRUE  1
    #define FALSE 0
    
    void main()
    {
      char* p;
      char* q;
      char xdata* xp = NULL;
    
      bit fail = FALSE;
    
      // This fails.
      if ( p = xp )
      {
        fail = TRUE;
      }
    
      // This is ok.
      if ( ( q = xp ) == NULL )
      {
        fail = TRUE;
      }
    }

    The first conditional, if ( p = xp ) assigns the value of an XDATA pointer xp to a GENERIC pointer p. The result is 0x010000 (assuming xp is NULL). This result is tested to see if it is non-zero (which it is) and the body of the if statement executes.

    The second conditional, if ( ( q = xp ) == NULL ) assigns the value of an XDATA pointer xp to a GENERIC pointer q. The result is 0x010000 (assuming xp is NULL). This result is tested to see if it is equal to NULL which is defined as (void *) 0. q has a value of 0x010000 and NULL has a value of 0x000000. The test fails and the body of the if statement does not execute.

    The typed-pointer (xp) is converted into a generic pointer (p) which includes the memory space information. A NULL pointer is a generic pointer without an explicit memory space. So, the comparison is not an apples to apples comparison.

    If NULL is cast to (void xdata *) NULL the if body executes. The reason is that the (void xdata *) NULL is a memory-specific pointer. For the comparison, it is implicitly cast to a generic pointer with the xdata memory space information.

    If you modify the original code with this type cast, you will see that all 3 bytes of the q and the NULL pointer are compared.

    All of this boils down to the fact that 8-bit embedded systems are NOT little PC or PowerPC programs. There is no operating system, no disk drive, and the architectures are goofy and require that you learn a little about the chips. The memory architecture and how it relates to NULL are just another part of that learning experience.

    Jon

    P.S.
    An interest effect is that NULL == (void idata *) NULL.
    But, (void xdata *) NULL != (void code *) NULL.

Reply
  • It's only Keil C, and particular it's memory-specific pointers, that make it hard.

    NULL is not difficult. It is the 8051 architecture that throws a wrench into things.

    The 8051 is not a vonNeumann architecture and it is not linear. In the 8051, you have:

    • 256 bytes of IDATA (which starts at address 0x00 and which is addressed using a single-byte (8-bit) pointer. In this memory a NULL pointer has the value 0x00. IDATA is accessed using an idata pointer (int idata *).

    • 64K bytes of XDATA (which starts at address 0x0000 and which is addressed using a two-byte (16-bit) pointer. In this memory model a NULL pointer has the value 0x0000. XDATA is accessed using an xdata pointer (int xdata *).

    • 64K bytes of CODE (which starts at address 0x0000 and which is addressed using a two-byte (16-bit) pointer. In this memory model a NULL pointer has the value 0x0000. CODE is accessed using a code pointer (int code *).

    If you only deal with typed pointers or memory-specific pointers (idata *, xdata *, code *, ...) NULL works perfectly. But, if you only have memory-specific pointers (and no generic pointers) you must have multiple copies of library functions that accept pointers. For example, the (generic pointer) strcpy function is very well known:

    strcpy (char *d, char *s);

    If we supported only memory-specific pointers, we would require at least six different strcpy routines:

    strcpy_ii (char idata *d, char idata *s);
    strcpy_ix (char idata *d, char xdata *s);
    strcpy_ic (char idata *d, char code *s);
    
    strcpy_xi (char xdata *d, char idata *s);
    strcpy_xx (char xdata *d, char xdata *s);
    strcpy_xc (char xdata *d, char code *s);

    That would make the library HUGE and also make it really inconvenient to write embedded programs. But, NULL would always work just GREAT!

    Generic pointers are used to solve several problems--primarily to make the address space linear. Basically, the idea is that each memory area needs a memory selector (or segment) byte. So, IDATA is 0x00, XDATA is 0x01, and CODE is 0xFF.

    So...

    • A generic pointer to code address 0x0000 is 0xFF0000.

    • A generic pointer to xdata address 0x0000 is 0x010000.

    • A generic pointer to idata address 0x0000 is 0x000000.

    Now the question is which one of these is a pointer to offset 0 (in a particular memory space) and which is NULL? Based on the definition of NULL (which is defined as ((void *) 0) ) the only candidate for NULL is a generic pointer to idata address 0x00.

    The correct change would be to not handle (pointer == 0) or (pointer == NULL) by casting the pointer to some other type, but rather by casting the zero to the given pointer's type.

    No change is required. The Keil compiler already does this. The following program illustrates that.

    #include <REG52.H>
    #include <stdio.h>
    
    void main (void)
    {
    char idata *ip = NULL;
    char xdata *xp = NULL;
    char code *cp = NULL;
    char *gp = NULL;
    
    SCON  = 0x50;		        /* SCON: mode 1, 8-bit UART, enable rcvr      */
    TMOD |= 0x20;               /* TMOD: timer 1, mode 2, 8-bit reload        */
    TH1   = 221;                /* TH1:  reload value for 1200 baud @ 16MHz   */
    TR1   = 1;                  /* TR1:  timer 1 run                          */
    TI    = 1;                  /* TI:   set TI to send first char of UART    */
    
    if (ip == NULL) printf ("IP is NULL\n");
    if (xp == NULL) printf ("XP is NULL\n");
    if (cp == NULL) printf ("CP is NULL\n");
    if (gp == NULL) printf ("GP is NULL\n");
    
    if ((gp = ip) == NULL) printf ("(GP = IP) is NULL\n");
    if ((gp = xp) == NULL) printf ("(GP = XP) is NULL\n");
    if ((gp = cp) == NULL) printf ("(GP = CP) is NULL\n");
    
    while (1);
    }

    The original question refered to the following code (to which I added #defines):

    #define TRUE  1
    #define FALSE 0
    
    void main()
    {
      char* p;
      char* q;
      char xdata* xp = NULL;
    
      bit fail = FALSE;
    
      // This fails.
      if ( p = xp )
      {
        fail = TRUE;
      }
    
      // This is ok.
      if ( ( q = xp ) == NULL )
      {
        fail = TRUE;
      }
    }

    The first conditional, if ( p = xp ) assigns the value of an XDATA pointer xp to a GENERIC pointer p. The result is 0x010000 (assuming xp is NULL). This result is tested to see if it is non-zero (which it is) and the body of the if statement executes.

    The second conditional, if ( ( q = xp ) == NULL ) assigns the value of an XDATA pointer xp to a GENERIC pointer q. The result is 0x010000 (assuming xp is NULL). This result is tested to see if it is equal to NULL which is defined as (void *) 0. q has a value of 0x010000 and NULL has a value of 0x000000. The test fails and the body of the if statement does not execute.

    The typed-pointer (xp) is converted into a generic pointer (p) which includes the memory space information. A NULL pointer is a generic pointer without an explicit memory space. So, the comparison is not an apples to apples comparison.

    If NULL is cast to (void xdata *) NULL the if body executes. The reason is that the (void xdata *) NULL is a memory-specific pointer. For the comparison, it is implicitly cast to a generic pointer with the xdata memory space information.

    If you modify the original code with this type cast, you will see that all 3 bytes of the q and the NULL pointer are compared.

    All of this boils down to the fact that 8-bit embedded systems are NOT little PC or PowerPC programs. There is no operating system, no disk drive, and the architectures are goofy and require that you learn a little about the chips. The memory architecture and how it relates to NULL are just another part of that learning experience.

    Jon

    P.S.
    An interest effect is that NULL == (void idata *) NULL.
    But, (void xdata *) NULL != (void code *) NULL.

Children
  • It's always interesting to discuss whether an implementation of a C compiler conforms to the standard or not. Here is my 2 cents.
    The ISO C 1999 says:

    An integer constant expression with the value 0, or such an expression cast to type
    void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
    ...
    Conversion of a null pointer to another pointer type yields a null pointer of that type. Any two null pointers shall compare equal.


    Let's see if C51 conforms to that. Forgive me if I make mistakes here, since I only did C166.
    It's easy to satisfy the first requirement: a null pointer can't point to an object in C program. Just make sure that the linker doesn't put anything to where the null pointers point.
    The second requirement:

    if ( (void*)0 != (void xdata*)((void*)0) )
        printf("Oops! This is not ISO C behaviour.");
    
    From the previous discussion I got the impression that this code will do the 'oops'.
    My point is that it's probably OK to break standard conformance in some places if it's justified by performance/simplicity/etc considerations. After all, if one wants standard C behaviour, they can limit themselves to using generic pointers only.

    - mike

  • My point is that it's probably OK to break standard conformance in some places if it's justified by performance/simplicity/etc considerations. After all, if one wants standard C behaviour, they can limit themselves to using generic pointers only.

    I guess that this is the nub of the matter. If you write standard C code, you will always be getting generic pointers and, hence, standard C behaviour. Memory specific keywords are Keil extensions and if you choose to use them it is necessary to understand how they work in some detail.

  • Now the question is which one of these is a pointer to offset 0 (in a particular memory space) and which is NULL?

    From the point of view of the C standard, I think it's pretty clear: they're all NULL, and should compare equal to any null pointer.

    Based on the definition of NULL (which is defined as ((void *) 0) ) the only candidate for NULL is a generic pointer to idata address 0x00.

    You're thinking of (generic) pointers as integers, it seems. But they aren't numbers. They're abstract thingies that behave somewhat, but not quite like numbers.

    Actually, the C standard does not define NULL as (void *)0, either. #define NULL 0 would be every bit as valid.

    My point would be that in a comparison of the type

    (generic pointer == 0)
    or
    (generic pointer == NULL)

    If pointer is being compared to a null pointer, the compiled code should ignore the "memory class" or "segment" byte.

    If you apply the rules of standard C, and for a moment assume that memory-specific pointers should behave like "pointers" as known by the standard, then the rules about null pointers and null pointer constants strictly require that

    	(void *)(xdata char *)0 == 0

    because ((xdata char *)0) is a valid null pointer by construction, and casting a NULL pointer to another pointer type still gives a null pointer.

  • Jon,

    Thanks so much for this posting. It really hit home regarding the NULL value problem even after I had read the section on generic pointers and conversion.
    The issue to me is that during the comparison I am looking at the memory-type, which I do not care about. Here is a suggestion that might work.

    char * var;
    if ((unsigned short) var == (unsigned short) NULL) {}
    
    This will strip out the memory types on both sides and then a valid comparison can be made. What do you think?
    I am interested in getting feedback as my pointers could be any memory-type and I would like a generic means of checking for NULL.

    Robert

  • This will strip out the memory types on both sides and then a valid comparison can be made. What do you think?

    I'd probably do something like this:

    #define ISNULL(x)  (((unsigned) (void *) (x)) == 0)
    
    void * p;  // gerneric pointer
    .
    .
    .
    if (ISNULL(p))  {
      // NULL pointer detected
      }

    This has the effect of removing the memory selector and it's really obvious what you are testing.

    Jon

  • It would be worth you taking a look here:

    http://www.keil.com/update/_docs/releasenotes/c51v709.htm

    as there have been a number of bug fixes throughout version 7.x releases relating to comparisons between generic/far pointers and NULL.

    Stefan